001package org.hl7.fhir.r5.renderers;
002
003import java.io.File;
004import java.io.FileInputStream;
005import java.io.IOException;
006import java.io.InputStream;
007import java.util.ArrayList;
008import java.util.HashMap;
009import java.util.HashSet;
010import java.util.List;
011import java.util.Map;
012import java.util.Set;
013
014import javax.xml.parsers.DocumentBuilder;
015import javax.xml.parsers.DocumentBuilderFactory;
016import javax.xml.parsers.ParserConfigurationException;
017
018import org.hl7.fhir.exceptions.FHIRException;
019import org.hl7.fhir.r5.conformance.profile.ProfileUtilities;
020import org.hl7.fhir.r5.conformance.profile.SnapshotGenerationPreProcessor;
021import org.hl7.fhir.r5.context.ContextUtilities;
022import org.hl7.fhir.r5.context.IWorkerContext;
023import org.hl7.fhir.r5.elementmodel.LanguageUtils;
024import org.hl7.fhir.r5.extensions.ExtensionDefinitions;
025import org.hl7.fhir.r5.extensions.ExtensionUtilities;
026import org.hl7.fhir.r5.model.CanonicalType;
027import org.hl7.fhir.r5.model.CodeableConcept;
028import org.hl7.fhir.r5.model.Coding;
029import org.hl7.fhir.r5.model.DataType;
030import org.hl7.fhir.r5.model.ElementDefinition;
031import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionBindingComponent;
032import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent;
033import org.hl7.fhir.r5.model.Enumerations.BindingStrength;
034import org.hl7.fhir.r5.model.MarkdownType;
035import org.hl7.fhir.r5.model.Quantity;
036import org.hl7.fhir.r5.model.StructureDefinition;
037import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind;
038import org.hl7.fhir.r5.model.ValueSet;
039import org.hl7.fhir.r5.renderers.utils.RenderingContext;
040import org.hl7.fhir.r5.renderers.utils.RenderingContext.KnownLinkType;
041
042import org.hl7.fhir.r5.utils.UserDataNames;
043import org.hl7.fhir.utilities.*;
044import org.hl7.fhir.utilities.i18n.RenderingI18nContext;
045import org.hl7.fhir.utilities.json.model.JsonElement;
046import org.hl7.fhir.utilities.json.model.JsonObject;
047import org.hl7.fhir.utilities.xhtml.NodeType;
048import org.hl7.fhir.utilities.xhtml.XhtmlComposer;
049import org.hl7.fhir.utilities.xhtml.XhtmlNode;
050import org.hl7.fhir.utilities.xml.XMLUtil;
051import org.w3c.dom.Document;
052import org.w3c.dom.Element;
053import org.xml.sax.SAXException;
054
055public class ClassDiagramRenderer {
056  private String defnFile;
057  private StructureDefinition defnSD;
058
059  public class LinkInfo {
060
061    private boolean use;
062    private String pathData;
063    private String diamondTransform;
064    private String diamondPoints;
065  }
066
067  private static final String SLICE_COLOR = "#62A856";
068
069  public enum ClassItemMode {
070    NORMAL, SLICE;
071  }
072
073  private static final double CHAR_RATIO = 4.4;
074
075  public static class PointSpec {
076    private double x;
077    private double y;
078    public PointSpec(double x, double y) {
079      super();
080      this.x = x;
081      this.y = y;
082    }
083    public double getX() {
084      return x;
085    }
086    public double getY() {
087      return y;
088    }    
089  }
090
091  private RenderingContext rc;
092  private IWorkerContext context;
093  private ContextUtilities cutils; 
094  private ProfileUtilities putils; 
095  private Map<String, PointSpec> layout;
096  private Map<String, LinkInfo> linkLayouts;
097  private String sourceFolder;  
098  private String destFolder;  
099  private double minx = 0;
100  private double miny = 0;
101  private String prefix;
102  private String diagramId;
103  private Map<String, ClassItem> classes = new HashMap<String, ClassItem>();
104  private boolean attributes;
105  private boolean innerClasses;
106  private boolean constraintMode;
107  private int nc = 0;
108  private List<Link> links = new ArrayList<Link>();
109  private List<String> classNames = new ArrayList<String>();  
110  private String lang;
111
112  /**
113   * 
114   * @param sourceFolder - where to look for existing designs
115   * @param destFolder - where to generate .svg and .html for diagram review
116   * @param diagramId - the id of the diagram (goes in the filenames and the diagram itself)
117   * @param prefix - a prefix to put on all ids to ensure anchor names don't clash. 
118   * @param rc - rendering context
119   * @param lang
120   * 
121   * @throws IOException
122   */
123  public ClassDiagramRenderer(String sourceFolder, String destFolder, String diagramId, String prefix, RenderingContext rc, String lang) throws IOException {
124    this.sourceFolder = sourceFolder;
125    this.destFolder = destFolder;
126    this.rc = rc;
127    this.context = rc.getContext();
128    this.cutils = rc.getContextUtilities();
129    this.putils = rc.getProfileUtilities();
130    File f = new File(destFolder);
131    if (!f.exists()) {
132      FileUtilities.createDirectory(destFolder);
133    }
134    layout = new HashMap<String, ClassDiagramRenderer.PointSpec>();
135    linkLayouts = new HashMap<String, ClassDiagramRenderer.LinkInfo>();
136    if (diagramId == null) {
137      throw new Error("An id is required");
138    }
139    this.diagramId = diagramId;
140    this.prefix = (prefix == null ? "" : prefix);
141    this.lang = lang;
142  }
143
144  public boolean hasSource() {
145    try {
146      File f = new File(Utilities.path(sourceFolder, diagramId+".svg"));
147      return f.exists(); 
148    } catch (IOException e) {
149      return false;
150    }   
151  }
152  
153  public String buildClassDiagram(JsonObject control) throws Exception {
154    File f = new File(Utilities.path(sourceFolder, diagramId+".svg"));
155    if (f.exists()) {
156      parseSvgFile(f, f.getAbsolutePath());
157    }
158
159    attributes = control.asBoolean("attributes");
160    innerClasses = !control.asBoolean("no-inner-classes");
161    classNames = control.forceArray("classes").asStrings();
162    XhtmlNode doc = new XhtmlNode(NodeType.Element, "div");
163    XhtmlNode svg = doc.svg();
164    
165    minx = 0;
166    miny = 0;
167
168    Point size = determineMetrics(classNames);
169    adjustAllForMin(size);
170    svg.attribute("id", prefix+"n"+(++nc));
171    svg.attribute("version", "1.1");
172    svg.attribute("width", Integer.toString(Utilities.parseInt(control.forceObject("size").asString("width"), (int) size.x)));
173    svg.attribute("height", Integer.toString(Utilities.parseInt(control.forceObject("size").asString("height"), (int) size.y)));
174
175    shadowFilter(svg);
176    drawElement(svg, classNames);
177    countDuplicateLinks();
178    XhtmlNode insertionPoint = svg.getChildNodes().get(0);
179    for (Link l : links) {
180      drawLink(svg, l, insertionPoint);
181    }
182
183    String s = new XhtmlComposer(true, true).compose(doc.getChildNodes());
184    produceOutput("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+s);
185    return s; 
186  }
187
188  public String buildClassDiagram(StructureDefinition sd, String defnFile) throws FHIRException, IOException {
189    this.defnFile = defnFile;
190    this.defnSD = sd;
191
192    File f = new File(Utilities.path(sourceFolder, diagramId+".svg"));
193    if (f.exists()) {
194      parseSvgFile(f, f.getAbsolutePath());
195    }
196
197    attributes = true;
198    innerClasses = true;
199    classNames.add(sd.getName());
200    XhtmlNode doc = new XhtmlNode(NodeType.Element, "div");
201    XhtmlNode svg = doc.svg();
202    
203    minx = 0;
204    miny = 0;
205
206    Point size = determineClassMetrics(sd);
207    adjustAllForMin(size);
208    svg.attribute("id", prefix+"n"+(++nc));
209    svg.attribute("version", "1.1");
210    svg.attribute("width", Double.toString(size.x));
211    svg.attribute("height", Double.toString(size.y));
212
213    shadowFilter(svg);
214    drawClassElement(svg, sd);
215    countDuplicateLinks();
216    XhtmlNode insertionPoint = svg.getChildNodes().get(0);
217    for (Link l : links) {
218      drawLink(svg, l, insertionPoint);
219    }
220
221    String s = new XhtmlComposer(true, true).compose(doc.getChildNodes());
222    produceOutput("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+s);
223    return s; 
224  }
225
226  private void produceOutput(String s) throws IOException {
227    if ("".equals(prefix)) {
228      String svg = standaloneSVG(s);
229      String html = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
230          + "<!DOCTYPE HTML>\n"
231          + "<html xml:lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"en\">\n"
232          + "  <head>\n"
233          + "    <meta content=\"text/html;charset=utf-8\" http-equiv=\"Content-Type\"/>\n"
234          + "    <title>"+diagramId+" Class Diagram</title>\n"
235          + "    <link href=\"assets/fhir.css\" rel=\"stylesheet\"/>\n"
236          + "  </head>\n"
237          + "  <body>\n"
238          + "  <h2>Embedded SVG</h2>\n"
239          + s+"\r\n"
240          + "  <h2>External SVG</h2>\n"
241          + "    <embed src=\""+diagramId+".svg\" type=\"image/svg+xml\">\n"
242          + "  </body>\n"
243          + "</html>";
244
245      FileUtilities.stringToFile(svg, Utilities.path(destFolder, diagramId+".svg"));
246      FileUtilities.stringToFile(html, Utilities.path(destFolder, diagramId+".html"));
247    }
248  }
249
250  private String standaloneSVG(String s) {
251    String css = "<?xml-stylesheet href=\"assets/fhir.css\" type=\"text/css\"?>";
252    int i = s.indexOf(">")+1;
253    s = s.substring(0, i)+css+s.substring(i);
254    return s;
255  }
256
257  private void countDuplicateLinks() {
258    for (int i = 0; i < links.size(); i++) {
259      Link l = links.get(i);
260      if (l.count == 0) {
261        int c = 0;
262        for (int j = i+1; j < links.size(); j++) {
263          Link l2 = links.get(j);
264          if ((l2.source == l.source && l2.target == l.target) ||
265              (l2.source == l.target && l2.target == l.source))
266            c++;
267        }     
268        l.count = c;
269        if (c > 0) {
270          int k = 0;
271          for (int j = i+1; j < links.size(); j++) {
272            Link l2 = links.get(j);
273            if ((l2.source == l.source && l2.target == l.target) ||
274                (l2.source == l.target && l2.target == l.source) ) {
275              k++;
276              l2.count = c;
277              l2.index = k;
278            }
279          }     
280        }
281      }
282    }
283  }
284
285  private void adjustAllForMin(Point size) {
286    size.x = size.x - minx;
287    size.y = size.y - miny;
288    for (ClassItem t : classes.values()) {
289      t.left = t.left - minx;
290      t.top = t.top - miny;
291    }
292  }
293
294  private void shadowFilter(XhtmlNode svg) throws IOException {
295    var defs = svg.addTag("defs");
296    var filter = defs.addTag("filter").attribute("id", prefix+"shadow"+diagramId).attribute("x", "0").attribute("y", "0").attribute("width", "200%").attribute("height", "200%");
297    var feOffset = filter.addTag("feOffset").attribute("result", "offOut").attribute("in", "SourceGraphic").attribute("dx", "3").attribute("dy", "3");    
298    var feColorMatrix = filter.addTag("feColorMatrix").attribute("result", "matrixOut").attribute("in", "offOut").attribute("type", "matrix").attribute("values", "0.2 0 0 0 0 0 0.2 0 0 0 0 0 0.2 0 0 0 0 0 1 0");    
299    var feGaussianBlur = filter.addTag("feGaussianBlur").attribute("result", "blurOut").attribute("in", "matrixOut").attribute("stdDeviation", "2");    
300    var feBlend = filter.addTag("feBlend").attribute("in", "SourceGraphic").attribute("in2", "blurOut").attribute("mode", "normal");    
301  }
302
303  private void parseSvgFile(File f, String name) throws FHIRException, IOException {
304    Document svg;
305    try {
306      svg = XMLUtil.parseFileToDom(f.getAbsolutePath());
307    } catch (ParserConfigurationException | SAXException | IOException e) {
308      throw new IOException(e);
309    }
310    readElement(svg.getDocumentElement(), null);
311    fixLayout();
312  }
313
314  private void fixLayout() {
315    double minx = Integer.MAX_VALUE;
316    double miny = Integer.MAX_VALUE;
317    for (PointSpec ps : layout.values()) {
318      if (ps.getX() < minx) {
319        minx = ps.getX();
320      }
321      if (ps.getY() < miny) {
322        miny = ps.getY();
323      }
324    }
325    for (String n : layout.keySet()) {
326      PointSpec ps = layout.get(n);
327      PointSpec nps = new PointSpec(ps.getX() - minx, ps.getY() - miny);
328      layout.put(n, nps);
329    }
330  }
331
332  private void readElement(Element e, Element p) {
333    String id = e.getAttribute("id");
334    if (!Utilities.noString(id) && Character.isUpperCase(id.charAt(0))) {
335      switch (e.getNodeName()) {
336      case "rect":
337      case "text":
338        double x = Double.valueOf(e.getAttribute("x"));
339        double y = Double.valueOf(e.getAttribute("y"));
340        if (p.hasAttribute("transform")) {
341          String s = p.getAttribute("transform");
342          if (s.startsWith("translate(")) {
343            String[] sp = s.substring(10, s.length()-1).split("\\,");
344            double tx = Double.valueOf(sp[0]);
345            double ty = sp.length > 1 ? Double.valueOf(sp[1]) : 0;
346            x = x + tx;
347            y = y + ty;
348          }
349        }
350        layout.put(id, new PointSpec(x, y));
351        break;
352      case  "path":
353        String d = e.getAttribute("d");
354        if (d != null) { // 'm ' - means that inkscape has edited it 
355          linkLayout(id).pathData = d;
356          linkLayout(id).use = d.startsWith("m ");
357        }
358        break;
359      case "polygon": 
360        String v = e.getAttribute("transform");
361        if (v != null) {
362          linkLayout(id.replace("-lpolygon", "")).diamondTransform = v;
363        }
364        v = e.getAttribute("points");
365        if (v != null) {
366          linkLayout(id.replace("-lpolygon", "")).diamondPoints = v;
367        }
368        break;
369      }
370    }
371    Element c = XMLUtil.getFirstChild(e);
372    while (c != null) {
373      readElement(c, e);
374      c = XMLUtil.getNextSibling(c);
375    }
376  }
377
378
379  private LinkInfo linkLayout(String id) {
380    if (!linkLayouts.containsKey(id)) {      
381      LinkInfo res = new LinkInfo();
382      linkLayouts.put(id, res);
383    }
384    return linkLayouts.get(id);
385  }
386
387
388  private static final double LINE_HEIGHT = 16;
389  private static final double HEADER_HEIGHT = 20;
390  private static final double GAP_HEIGHT = 4;
391  private static final double LEFT_MARGIN = 6;
392  private static final double SELF_LINK_HEIGHT = 25;
393  private static final double SELF_LINK_WIDTH = 60;
394  private static final double DUPLICATE_GAP = 50;
395  private static final double MARGIN_X = 100;
396  private static final double MARGIN_Y = 10;
397  private static final double WRAP_INDENT = 20;
398  private static final int LINE_MAX = 60;
399  public static final int MAX_NEG = -1000000;
400  private static final double UML_ROW_HEIGHT = 100;
401
402  private enum PointKind {
403    unknown, left, right, top, bottom;
404  } 
405  private class Point {
406    private PointKind kind;
407    public Point(double x, double y, PointKind kind) {
408      this.x = x;
409      this.y = y;
410      this.kind = kind;
411    }
412    private double x;
413    private double y;
414    private String toPoint() {
415      return Double.toString(x)+","+Double.toString(y);
416    }
417  }
418
419  private class ClassItem {
420    public ClassItem(double left, double top, double width, double height, String id, String name, ClassItemMode mode) {
421      this.left = left;
422      this.top = top;
423      this.width = width;
424      this.height = height;          
425      this.id = id;
426      this.name = name;
427      this.mode = mode;
428      if (layout != null && layout.containsKey(id)) {
429        this.left = layout.get(id).getX();
430        this.top = layout.get(id).getY();
431      }
432    }
433    private double left;
434    private double top;
435    private double width;
436    private double height;
437    private String id;
438    private String name;
439    private ClassItemMode mode;
440    public double right() {
441      return left + width;
442    }
443    public double centerH() {
444      return left + width / 2;
445    }
446    public double centerV() {
447      return top + height / 2;
448    }
449    public double bottom() {
450      return top + height;
451    }
452    public String getId() {
453      return id;
454    }
455    public String getName() {
456      return name;
457    }
458    public ClassItemMode getMode() {
459      return mode;
460    }
461  }
462
463  private class Segment {
464
465    public final Point start, end;
466    public final boolean isVertical; 
467    public final double slope, intercept; 
468
469    public Segment(Point start, Point end) {
470      this.start = start;
471      this.end = end;
472      //set isVertical, which indicates whether this Line 
473      //is vertical or not on the coordinate plane
474      if (start.x == end.x)
475        isVertical = true;
476      else
477        isVertical = false;
478
479      //set slope and intercept
480      if (!isVertical){
481        slope = (this.start.y - this.end.y) / (this.start.x - this.end.x);
482        intercept = (this.end.x * this.start.y - this.start.x * this.end.y ) /(this.start.x - this.end.x);
483      }
484      else {
485        slope = Double.MAX_VALUE;
486        intercept = - Double.MAX_VALUE;
487      }
488    }
489  }
490  private class LineStatus {
491    int line = 0;
492    int length = 0;
493    String current = "";
494    List<String> list = new ArrayList<String>();
495
496    public String see(String s) {
497      length = length + s.length();
498      current = current + s;
499      return s;
500    }
501
502    public void close() {
503      line++;
504      list.add(current);
505      length = 0;
506      current = "";
507    }
508
509    public void check(XhtmlNode html, XhtmlNode div, double left, double top, int l, String link) throws IOException {
510      if (length + l > LINE_MAX-2) { // always leave space for one or two
511        close();
512        html.attribute("height", Double.toString(top + LINE_HEIGHT * line));
513        div.br();
514        see("      ");
515        div.nbsp();
516        div.nbsp();
517        div.nbsp();
518        div.nbsp();
519        div.nbsp();
520        div.nbsp();
521      }
522    }   
523  }
524  private enum LinkType {SPECIALIZATION, CONSTRAINT, COMPOSITION, SLICE};
525  private class Link {
526    private String path;
527    private String description;
528    public Link(ClassItem source, ClassItem target, LinkType type, String name, String cardinality, PointKind kind, String path, String description) {
529      this.source = source;
530      this.target = target;
531      this.type = type;
532      this.name = name;
533      this.cardinality = cardinality;
534      this.kind = kind;
535      this.path = path;
536      this.description = description;
537    }
538    private LinkType type;
539    private ClassItem source;
540    private ClassItem target;
541    private String name;
542    private String cardinality;
543    private PointKind kind;
544    private int count;
545    private int index;
546  }
547
548
549  private Point determineMetrics(List<String> classNames) throws Exception {
550    double width = textWidth("Element") * 1.8;
551    double height = HEADER_HEIGHT + GAP_HEIGHT*2;
552    //    if ("true".equals(ini.getStringProperty("diagram", "element-attributes"))) {
553    //      height = height + LINE_HEIGHT + GAP_HEIGHT;
554    //      width = textWidth("extension : Extension 0..*");
555    //    }
556
557    Point p = new Point(0, 0, PointKind.unknown);
558    ClassItem item = new ClassItem(p.x, p.y, width, height, diagramId, null, ClassItemMode.NORMAL);
559    classes.put(null, item);
560    double x = item.right()+MARGIN_X;
561    double y = item.bottom()+MARGIN_Y;
562    if (classNames != null) {
563      for (String cn : classNames) {
564        StructureDefinition sd = context.fetchResource(StructureDefinition.class, cn);
565        if (sd == null) {
566          sd = cutils.fetchStructureByName(cn);
567        }
568        if (sd == null) {
569          throw new FHIRException("Unable to find class '"+cn+"'");
570        }
571        ElementDefinition ed = sd.getSnapshot().getElementFirstRep();
572        StructureDefinition base = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition());
573        p = determineMetrics(sd, ed, item, ed.getName(), null, base, ClassItemMode.NORMAL);
574        x = Math.max(x, p.x+MARGIN_X);
575        y = Math.max(y, p.y+MARGIN_Y);
576      }
577    }
578    return new Point(x, y, PointKind.unknown);
579  }
580
581  private Point determineClassMetrics(StructureDefinition sd) throws FHIRException, IOException {
582    double width = textWidth("Element") * 1.8;
583    double height = HEADER_HEIGHT + GAP_HEIGHT*2;
584    //    if ("true".equals(ini.getStringProperty("diagram", "element-attributes"))) {
585    //      height = height + LINE_HEIGHT + GAP_HEIGHT;
586    //      width = textWidth("extension : Extension 0..*");
587    //    }
588
589    Point p = new Point(0, 0, PointKind.unknown);
590    ClassItem item = new ClassItem(p.x, p.y, width, height, diagramId, null, ClassItemMode.NORMAL);
591    classes.put(null, item);
592    double x = item.right()+MARGIN_X;
593    double y = item.bottom()+MARGIN_Y;
594    ElementDefinition ed = sd.getSnapshot().getElementFirstRep();
595    StructureDefinition base = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition());
596    p = determineMetrics(sd, ed, item, ed.getName(), null, base, ClassItemMode.NORMAL);
597    x = Math.max(x, p.x+MARGIN_X);
598    y = Math.max(y, p.y+MARGIN_Y);
599    return new Point(x, y, PointKind.unknown);
600  }
601
602
603  private Point determineMetrics(StructureDefinition sd, ElementDefinition ed, ClassItem source, String path, String name, StructureDefinition base, ClassItemMode mode) throws FHIRException, IOException {
604
605    List<ElementDefinition> children = putils.getChildList(sd, ed);
606    String n = name;
607    if (n == null) {
608      if (!path.contains(".")) {
609        n = sd.getName();
610      } else if (!constraintMode && !children.isEmpty()) {
611        String[] p = path.split("\\.");
612        StringBuilder b =  new StringBuilder();
613        for (String s : p) {
614          b.append(Utilities.capitalize(s));
615        }
616        n = b.toString();
617      } else if (constraintMode && ed.getType().size() == 1) {
618        n = ed.getTypeFirstRep().getWorkingCode();
619      } else {
620        n = "DataType"; 
621      }
622    }
623
624    String t = n;
625    if (path.contains(".")) {
626      if (ed.getType().size() == 1) {
627        if (!"Base".equals(ed.getTypeFirstRep().getWorkingCode())) {
628          t = t + " ("+ed.getTypeFirstRep().getWorkingCode()+")";
629        }
630      } else {
631        t = "";
632      }
633    } else if (base != null && !classNames.contains(base.getName())) {
634       t = t + " ("+base.getName()+")";
635    }
636    if (sd.hasExtension(ExtensionDefinitions.EXT_RESOURCE_INTERFACE)) {
637      t = t + " ĞInterfaceğ";
638    }
639    double width = textWidth(t) * 1.8;
640    //double width = textWidth(e.getName()) * 1.8 + (isRoot ? textWidth(" (Resource)") : 0);
641    double height;
642    if (attributes) {
643      int i = 0;
644      for (ElementDefinition c : children) 
645        if (inScope(c) && countsAsAttribute(sd, c)) {
646          String[] texts = textForAttribute(sd, c);
647          i = i + texts.length;
648          double w = textWidth(texts[0]);
649          for (int j = 1; j < texts.length; j++)
650            w = Math.max(w, textWidth(texts[j]));
651          if (w > width)
652            width = w;
653        }
654      height = HEADER_HEIGHT + GAP_HEIGHT*2 + LINE_HEIGHT * i + GAP_HEIGHT * 2;
655    }  else
656      height = HEADER_HEIGHT + GAP_HEIGHT*2;
657
658//    if (control.forceObject("directions").has(path)) {
659//      JsonElement uml = control.forceObject("directions").get(path);
660//      if (uml.isJsonObject()) {
661//        JsonObject svg = (JsonObject) uml;
662//        ed.setUserData(UserDataNames.SvgLeft, svg.asInteger("left"));
663//        ed.setUserData(UserDataNames.SvgTop, svg.asInteger("top"));
664//      } else {      
665//        ed.setUserData(UserDataNames.UmlDir, uml.asString());
666//      }
667//    }
668
669    Point p = new Point(getSvgLeft(ed), getSvgLeft(ed), PointKind.unknown);
670    if (p.y == MAX_NEG || p.x == MAX_NEG) {
671      if ("left".equals(ed.getUserString(UserDataNames.UmlDir))) {
672        p.x = source.left - 120 - width;
673        p.y = source.centerV() - height / 2;
674        p = findEmptyPlace(p, width, height, 0, 80);
675      } else if ("right".equals(ed.getUserString(UserDataNames.UmlDir))) {
676        p.x = source.right() + 120;
677        p.y = source.centerV() - height / 2;
678        p = findEmptyPlace(p, width, height, 0, 80);
679      } else if ("up".equals(ed.getUserString(UserDataNames.UmlDir))) {
680        p.x = source.centerH() - width / 2;
681        p.y = source.top - height - 80;
682        p = findEmptyPlace(p, width, height, 80, 0);
683      } else if ("down".equals(ed.getUserString(UserDataNames.UmlDir))) {
684        p.x = source.centerH() - width / 2;
685        p.y = source.bottom() + 80;
686        p = findEmptyPlace(p, width, height, +80, 0);
687      } else {
688        p.y = 0;
689        p.x = 0;
690        p = findEmptyPlace(p, width, height, 80, 0);
691      }
692    }
693    miny = Math.min(miny, p.y);
694    minx = Math.min(minx, p.x);
695    ClassItem item = new ClassItem(p.x, p.y, width, height, path, n, mode);
696    classes.put(path, item);
697    double x = item.right()+MARGIN_X;
698    double y = item.bottom()+MARGIN_Y;
699
700    if (innerClasses) {
701      for (ElementDefinition c : children) {  
702        if (inScope(c) && countsAsRelationship(sd, c) && !c.hasSliceName()) {
703          if (c.hasContentReference()) {
704            String cr = c.getContentReference();
705            ClassItem target = classes.get(cr.substring(cr.indexOf("#")+1));
706            if (target == null) {
707              throw new Error("what?");
708            }
709            // do something about x and y?
710          } else {
711            p = determineMetrics(sd, c, item, path+"."+c.getName(), null, null, ClassItemMode.NORMAL);
712            x = Math.max(x, p.x+MARGIN_X);
713            y = Math.max(y, p.y+MARGIN_Y);
714            // do we find slices?
715            if (c.hasSlicing()) {
716              List<ElementDefinition> slices = getSlices(children, c);
717              for (ElementDefinition s : slices) {
718                p = determineMetrics(sd, s, item, path+"."+c.getName()+":"+s.getSliceName(), s.getSliceName(), null, ClassItemMode.SLICE);
719                x = Math.max(x, p.x+MARGIN_X);
720                y = Math.max(y, p.y+MARGIN_Y);
721              }
722            }
723          }
724        }
725      }
726    }
727    return new Point(x, y, PointKind.unknown);
728  }
729
730  private boolean countsAsRelationship(StructureDefinition sd, ElementDefinition c) {
731    return !countsAsAttribute(sd, c);
732  }
733
734  private boolean countsAsAttribute(StructureDefinition sd, ElementDefinition ed) {
735    if (ed.hasContentReference()) {
736      return false;
737    } else if (ed.prohibited()) {
738      return true;
739    } else if (ed.getType().isEmpty()) {
740      return true; // really shouldn't be the case
741    } else {
742      List<ElementDefinition> children = putils.getChildList(sd, ed, false);
743      if (ed.getType().size() == 1) {
744        StructureDefinition sdt = context.fetchTypeDefinition(ed.getTypeFirstRep().getWorkingCode());
745        if (sdt == null) {
746          return true; // also shouldn't happen
747        } else if (sdt.getKind() == StructureDefinitionKind.PRIMITIVETYPE) {
748          return true;
749        } else if (sdt.getAbstract()) {
750          return children.size() == 0;
751        } else if (sdt.getKind() == StructureDefinitionKind.COMPLEXTYPE && !"Base".equals(sdt.getName())) {
752          return !(constraintMode && (ed.hasSlicing() || ed.hasSliceName()));
753        } else {
754          return children.size() == 0;
755        }
756      } else {
757        return children.size() == 0 || (constraintMode && ed.hasSlicing());
758      }      
759    }
760  }
761
762  private List<ElementDefinition> getSlices(List<ElementDefinition> list, ElementDefinition focus) {
763    List<ElementDefinition> slices = new ArrayList<ElementDefinition>();
764    for (int i = list.indexOf(focus)+1; i < list.size(); i++) {
765      ElementDefinition ed = list.get(i);
766      if (ed.getPath().equals(focus.getPath()) && ed.hasSliceName()) {
767        slices.add(ed);
768      }
769    }
770    return slices;
771  }
772
773  private boolean inScope(ElementDefinition c) {
774    return !constraintMode || c.hasUserData(UserDataNames.SNAPSHOT_FROM_DIFF);
775  }
776
777  private Point findEmptyPlace(Point p, double width, double height, double dx, double dy) {
778    while (overlaps(p.x, p.y, width, height)) {
779      p.x = p.x + dx;
780      p.y = p.y + dy;
781      if (p.x > 600) {
782        p.y = p.y + UML_ROW_HEIGHT;
783        p.x = 0;
784      }
785    }
786    return p;
787  }
788
789  private boolean overlaps(double x, double y, double w, double h) {
790    for (ClassItem c : classes.values()) {
791      if ((inBounds(x, c.left, c.right()) || inBounds(x+w, c.left, c.right())) &&
792          (inBounds(y, c.top, c.bottom()) || inBounds(y+h, c.top, c.bottom())))
793        return true;
794      if ((inBounds(c.left, x, x+w) || inBounds(c.right(), x, x+w)) &&
795          (inBounds(c.top, y, y+h) || inBounds(c.bottom(), y, y+h)))
796        return true;
797    }
798    return false;
799  }
800
801  private boolean inBounds(double x, double x1, double x2) {
802    return (x1 < x2) ? (x >= x1 && x <= x2) : (x >= x2 && x <= x1);
803  }
804
805  private double getSvgLeft(ElementDefinition ed) {
806    Integer  i = (Integer) ed.getUserData(UserDataNames.SvgLeft);
807    return i == null ? MAX_NEG : i;
808  }
809
810  private double getSvgTop(ElementDefinition ed) {
811    Integer  i = (Integer) ed.getUserData(UserDataNames.SvgTop);
812    return i == null ? MAX_NEG : i;
813  }
814
815  private int addAttribute(XhtmlNode g, double left, double top, StructureDefinition sd, ElementDefinition e, String path, double height, double width) throws FHIRException, IOException  {
816    LineStatus ls = new LineStatus();
817    return addAttribute(g, left, top, sd, e, path, ls, height, width);
818  }
819
820  private int addAttribute(XhtmlNode g, double left, double top, StructureDefinition sd, ElementDefinition e, String path, LineStatus ls, double height, double width) throws FHIRException, IOException  {
821    if (e.getStandardsStatus() != null) {
822      var rect = g.svgRect(null);
823      rect.attribute("x", Double.toString(left+1));
824      rect.attribute("y", Double.toString(top-height+GAP_HEIGHT));
825      rect.attribute("id", prefix+"n"+(++nc));
826      rect.attribute("width", Double.toString(width-2));
827      rect.attribute("height", Double.toString(height));
828      rect.style("fill:"+e.getStandardsStatus().getColorSvg()+";stroke:black;stroke-width:0");
829    }
830    var html = g.htmlObject(left + LEFT_MARGIN + (ls.line == 0 ? 0 : WRAP_INDENT), top + LINE_HEIGHT * (ls.line-1), width-2, height); 
831    var div = html.div().attribute("xmlns", "http://www.w3.org/1999/xhtml");
832    div.attribute("class", "diagram-class-detail");
833    if (e.prohibited()) {
834      div.style("text-decoration: line-through");
835    }
836
837    var a = div.ah(urlForAttribute(sd, e, path)).style("text-decoration: none;");
838    a.attributeNN("title", getEnhancedDefinition(e));
839    a.tx(ls.see(e.getName()));
840
841    div.tx(ls.see(" : "));
842    if (e.hasContentReference()) {
843      encodeType(div, ls, e.getContentReference().substring(e.getContentReference().indexOf("#")+1));      
844    } else {
845      encodeType(div, ls, getTypeCodeForElement(e.getType()));
846    }
847    div.tx(ls.see(" ["+describeCardinality(e)+"]"));
848
849    if (!"0".equals(e.getMax())) {
850      if (constraintMode) { 
851        // in constraint mode, we're (usually) even more constrained for space, and we have a lot more kinds of constraints 
852        // to deal with 
853        Set<String> p = new HashSet<>();
854        Set<String> tp = new HashSet<>(); 
855        for (TypeRefComponent tr : e.getType()) {
856          for (CanonicalType ct : tr.getProfile()) {
857            p.add(ct.asStringValue());
858          }
859          for (CanonicalType ct : tr.getTargetProfile()) {
860            if (!ct.asStringValue().startsWith("http://hl7.org/fhir/StructureDefinition/")) {
861              tp.add(ct.asStringValue());
862            }
863          }
864        }        
865        flag(div, ls, !p.isEmpty(), "DP", "black", "#c787ff", rc.formatPhrase(RenderingContext.GENERAL_TYPE_PROFILE, CommaSeparatedStringBuilder.join(",", Utilities.sorted(p))), null);
866        flag(div, ls, !tp.isEmpty(), "TP", "black", "#c787ff", rc.formatPhrase(RenderingContext.GENERAL_TYPE_TARGET_PROFILE, CommaSeparatedStringBuilder.join(",", Utilities.sorted(tp))), null);
867        
868        flag(div, ls, e.getIsModifier(), "?!", "black", "white", rc.formatPhrase(RenderingContext.STRUC_DEF_MOD), null);
869//        flag(div, ls, e.getIsSummary(), "\u03A3", "white", "black", rc.formatPhrase(RenderingContext.STRUC_DEF_ELE_INCLUDED), null);
870        
871        if (e.getMustSupport() && e.hasExtension(ExtensionDefinitions.EXT_OBLIGATION_CORE, ExtensionDefinitions.EXT_OBLIGATION_TOOLS)) {
872          flag(div, ls, e.getMustSupport(), "SO", "white", "red", rc.formatPhrase(RenderingContext.STRUC_DEF_OBLIG_SUPP), null);        
873        } else if (e.hasExtension(ExtensionDefinitions.EXT_OBLIGATION_CORE, ExtensionDefinitions.EXT_OBLIGATION_TOOLS)) {
874          flag(div, ls, e.getMustSupport(), "O", "white", "red", rc.formatPhrase(RenderingContext.STRUC_DEF_OBLIG), null);
875        } else {
876          flag(div, ls, e.getMustSupport(), "S", "white", "red", rc.formatPhrase(RenderingContext.STRUC_DEF_ELE_MUST_SUPP), null);
877        }
878        flag(div, ls, e.getMustHaveValue(), "V", "black", "#f7a3ec", rc.formatPhrase(RenderingContext.STRUC_DEF_ELE), null);
879        flag(div, ls, e.hasValueAlternatives(), "?X", "black", "#f7a3ec", rc.formatPhrase(RenderingContext.STRUC_DEF_VALUE_ALT), null);
880        flag(div, ls, StructureDefinitionRenderer.hasNonBaseConstraints(e.getConstraint()) || StructureDefinitionRenderer.hasNonBaseConditions(e.getCondition()), "C", "black", "#7779e6", rc.formatPhrase(RenderingContext.STRUC_DEF_ELE_AFFECTED), null);
881        flag(div, ls, e.hasFixed(), "F", "black", "#95fc9c", rc.formatPhrase(RenderingContext.GENERAL_FIXED_VALUE, renderDT(e.getFixed())), null);
882        flag(div, ls, e.hasPattern(), "P", "black","#95fc9c", rc.formatPhrase(RenderingContext.GENERAL_PATTERN_VALUE, renderDT(e.getPattern())), null);
883        if (e.hasMinValue() || e.hasMaxValue()) {
884          if (e.hasMinValue() && e.hasMaxValue()) {
885            flag(div, ls, true, "L<<H", "black", "green", rc.formatPhrase(RenderingContext.GENERAL_VALUE_BOUNDED, e.getMaxLength()), null);
886          } else {
887            flag(div, ls, e.hasMaxValue(), "L<", "black", "#95fc9c", rc.formatPhrase(RenderingContext.GENERAL_VALUE_MIN, renderDT(e.getMinValue())), null);            
888            flag(div, ls, e.hasMaxValue(), "<H", "black", "#95fc9c", rc.formatPhrase(RenderingContext.GENERAL_VALUE_MAX, renderDT(e.getMaxValue())), null);
889          }
890        }
891        flag(div, ls, e.hasMaxLength(), "<L", "black", "#95fc9c", rc.formatPhrase(RenderingContext.GENERAL_MAX_LENGTH, e.getMaxLength()), null);
892        if (e.hasBinding()) {
893          ValueSet vs = context.fetchResource(ValueSet.class, path);
894          if (e.getBinding().getStrength() == BindingStrength.REQUIRED) {
895            flag(div, ls, true, "B!", "black", "#fad570", rc.formatPhrase(RenderingContext.GENERAL_REQUIRED_BINDING, describeVS(e.getBinding().getValueSet(), vs)), vsLink(e.getBinding().getValueSet(), vs));            
896          } else if (e.getBinding().getStrength() == BindingStrength.EXTENSIBLE) {
897            flag(div, ls, true, "B?", "black", "#fad570", rc.formatPhrase(RenderingContext.GENERAL_REQUIRED_BINDING, describeVS(e.getBinding().getValueSet(), vs)), vsLink(e.getBinding().getValueSet(), vs));                        
898          } 
899          flag(div, ls, e.hasExtension(ExtensionDefinitions.EXT_BINDING_ADDITIONAL), "B+", "black", "#fad570", rc.formatPhrase(RenderingContext.GENERAL_ADDITIONAL_BINDING), null);
900        }
901      } else {
902
903        boolean hasTS = !((e.getType().isEmpty()) || (e.getType().size() == 1 && !isReference(e.getType().get(0).getName())));
904        boolean hasBinding = e.hasBinding() && e.getBinding().getStrength() != BindingStrength.NULL;
905        if (hasTS || hasBinding) {
906          div.tx(ls.see(" \u00AB "));
907          if (hasTS) {
908            if (isReference(e.getTypeFirstRep().getWorkingCode()) && e.getType().size() == 1) {
909              boolean first = true;
910              for (CanonicalType p : e.getTypeFirstRep().getTargetProfile()) {
911                if (first)
912                  first = false;
913                else 
914                  div.tx(ls.see(" | "));
915                StructureDefinition sdt = context.fetchResource(StructureDefinition.class, p.asStringValue(), null, sd);
916                String s = sdt == null ? tail(p.asStringValue()) : sdt.getName();
917                ls.check(html, div, left, top, s.length(), null);
918                encodeType(div, ls, s);
919              }
920            } else {
921              boolean firstOuter = true;
922              for (TypeRefComponent t : e.getType()) {
923                if (firstOuter)
924                  firstOuter = false;
925                else 
926                  div.tx(ls.see(" | "));
927
928                ls.check(html, div, left, top, t.getName().length(), null);
929                encodeType(div, ls, t.getWorkingCode());
930              }
931            }
932          }
933          if (hasTS && hasBinding) {
934            div.tx(ls.see("; "));
935          }
936          if (hasBinding) {
937            ElementDefinitionBindingComponent b = e.getBinding();
938            ValueSet vs = context.fetchResource(ValueSet.class, b.getValueSet()); 
939            String name = vs != null ? vs.getName() : tail(b.getValueSet());
940            if (name.toLowerCase().endsWith(" codes"))
941              name = name.substring(0, name.length()-5);
942            if (name.length() > 30)
943              name = name.substring(0, 29)+"...";
944            String link = vs == null ? null : vs.getWebPath();
945            String suffix = "";
946            suffix = getBindingSuffix(b);
947            div.ahOrNot(link, b.getDescription()+" (Strength="+(b.getStrength() == null ? "null " : b.getStrength().getDisplay())+")").tx(ls.see(name+suffix));
948          }
949          div.tx(ls.see(" \u00BB"));
950
951        }
952      }
953    }
954    return ls.line;
955  }
956
957  private String urlForAttribute(StructureDefinition sd, ElementDefinition ed, String path) {
958    String fullPath = path+"."+ed.getName(); //.replace("[", "_").replace("]", "_");
959    if (!ed.getPath().equals(ed.getBase().getPath()) || (ed.getBase().hasPath() && sd.getDerivation() == StructureDefinition.TypeDerivationRule.CONSTRAINT)) {
960      StructureDefinition sdt = context.fetchTypeDefinition(head(ed.getBase().getPath()));
961      if (sdt != null && !ExtensionUtilities.hasExtension(sdt, ExtensionDefinitions.EXT_RESOURCE_INTERFACE)) {
962        sd = sdt;
963        fullPath = ed.getBase().getPath();
964      }
965    }
966    return (sd == defnSD && defnFile != null ? defnFile : sd.getWebPath())+"#"+fullPath;
967  }
968
969  private String getBindingSuffix(ElementDefinitionBindingComponent b) {
970    String suffix;
971    if (b.getStrength() == null) {
972      return "??";
973    }
974    switch (b.getStrength()) {
975    case EXAMPLE:
976      suffix = "??";
977      break;
978    case EXTENSIBLE:
979      suffix = "+";
980      break;
981    case PREFERRED:
982      suffix = "?";
983      break;
984    case REQUIRED:
985      suffix = "!";
986      break;
987    case NULL:
988    default:
989      suffix = "??";
990      break;
991    }
992    return suffix;
993  }
994
995  private boolean hasNonBaseProfile(List<CanonicalType> list) {
996    for (CanonicalType ct : list) {
997      if (!ct.asStringValue().startsWith("http://hl7.org/fhir/StructureDefinition/")) {
998        return true;
999      }
1000    }
1001    return false;
1002  }
1003
1004  private String vsLink(String url, ValueSet vs) {
1005    if (vs != null) {
1006      return vs.getWebPath();
1007    } else {
1008      return url;      
1009    }
1010  }
1011
1012  private String describeVS(String url, ValueSet vs) {
1013    if (vs != null) {
1014      return vs.present()+" ("+url+")";
1015    } else {
1016      return "("+url+")";      
1017    }
1018  }
1019
1020  private String renderDT(DataType dt) {
1021    if (dt == null) {
1022      return "";
1023    } else if (dt.isPrimitive()) {
1024      return dt.primitiveValue();
1025    } else {
1026      switch (dt.fhirType()) {
1027      case "Quantity" : return renderQty((Quantity) dt);
1028      case "Coding" : return renderCoding((Coding) dt);
1029      case "CodeableConcept" : return renderCC((CodeableConcept) dt);
1030      default:
1031        return new DataRenderer(rc).displayDataType(dt);
1032      }
1033    }
1034  }
1035
1036  private String renderCC(CodeableConcept cc) {
1037    StringBuilder b = new StringBuilder();
1038    boolean first = true;
1039    for (Coding c : cc.getCoding()) {
1040      if (first) first = false; else b.append(", ");
1041      b.append(renderCoding(c));
1042    }
1043    if (b.length() > 0 && cc.hasText()) {
1044      b.append(". ");
1045    }
1046    if (cc.hasText()) {
1047      b.append("text: ");
1048      b.append(cc.getText());
1049    }
1050    return b.toString();
1051  }
1052
1053  private String renderCoding(Coding c) {
1054    StringBuilder b = new StringBuilder();
1055    if (c.hasSystem()) {
1056      b.append(new DataRenderer(rc).displaySystem(c.getSystem()));
1057      if (c.hasCode()) {
1058        b.append("#");
1059      }
1060    }
1061    if (c.hasCode()) {
1062      b.append(c.getCode());
1063    }
1064    if (c.hasDisplay()) {
1065      b.append("\"");
1066      b.append(c.getDisplay());
1067      b.append("\"");
1068    }
1069    return b.toString();
1070  }
1071
1072  private String renderQty(Quantity qty) {
1073    StringBuilder b = new StringBuilder();
1074    if (qty.hasComparator()) {
1075      b.append(qty.getComparatorElement().asStringValue());
1076    }
1077    b.append(qty.getValueElement().asStringValue());
1078    if (qty.hasUnit()) {
1079      b.append(qty.getUnit());
1080    } else if (qty.hasCode()) {
1081      b.append(qty.getCode());      
1082    }
1083    return b.toString();
1084  }
1085
1086  private void flag(XhtmlNode x, LineStatus ls, Boolean show, String code, String foreColor, String backColor, String title, String url) throws IOException {
1087    if (show) { 
1088      // <a style="padding-left: 3px; padding-right: 3px; border: 1px maroon solid; font-weight: bold; color: #301212; background-color: #fdf4f4;" href="conformance-rules.html#constraints" title="This element has or is affected by some invariants">C</a>
1089      x.tx(" ");
1090      var xx = url == null ? x.span() : x.ah(url).style("text-decoration: none;");
1091      xx.style("padding-left: 3px; padding-right: 3px; font-weight: bold; font-family: verdana; color: "+foreColor+"; background-color: "+backColor);
1092      xx.attribute("title", title);
1093      xx.tx(code);
1094      ls.see(code+" ");
1095    }
1096  }
1097
1098  private String tail(String url) {
1099    if (Utilities.noString(url)) {
1100      return "";
1101    }
1102    return url.contains("/") ? url.substring(url.lastIndexOf("/")+1) : url;
1103  }
1104
1105  private String describeCardinality(ElementDefinition e) {
1106    String min = !e.hasMin() ? "" : e.getMinElement().asStringValue();
1107    String max = !e.hasMax() ? "" : e.getMax();
1108    return min + ".." + max;
1109  }
1110
1111  private int encodeType(XhtmlNode text, LineStatus ls, String tc)  throws FHIRException, IOException {
1112    if (tc == null) {
1113      return 0;
1114    } else if (tc.equals("*")) {
1115      var a = text.ah(Utilities.pathURL(rc.getLink(KnownLinkType.SPEC, true), "datatypes.html#open")).style("text-decoration: none;");;
1116      a.tx(ls.see(tc));
1117      return tc.length();
1118    } else if (tc.equals("Type")) {
1119      var a = text.ah(Utilities.pathURL(rc.getLink(KnownLinkType.SPEC, true), "formats.html#umlchoice")).style("text-decoration: none;");;
1120      a.tx(ls.see(tc));
1121      return tc.length();
1122    } else if (tc.startsWith("@")) { 
1123      var a = text.ah("@"+tc.substring(1)).style("text-decoration: none;");;
1124      a.tx(ls.see(tc));
1125      return tc.length();
1126    } else { 
1127      StructureDefinition sd = Utilities.isAbsoluteUrl(tc) ? context.fetchTypeDefinition(tc) : null;
1128      if (sd != null) {
1129        var a = text.ah(sd.getWebPath()).style("text-decoration: none;");;
1130        a.tx(ls.see(sd.getName()));
1131        return tc.length();
1132      }
1133      var a = text.ah(makeLink(tc)).style("text-decoration: none;");;
1134      a.tx(ls.see(tc));
1135      return tc.length();
1136    }
1137  }
1138
1139
1140  private String makeLink(String tc) {
1141    StructureDefinition sd = context.fetchTypeDefinition(tc);
1142    return sd == null ? null : sd.getWebPath();
1143  }
1144
1145  private String getEnhancedDefinition(ElementDefinition e) {
1146    String defn = pvt(e.getDefinitionElement());
1147    if (e.getIsModifier() && e.getMustSupport()) {
1148      return Utilities.removePeriod(defn) + " "+rc.formatPhrase(RenderingI18nContext.SDR_MOD_SUPP);
1149    } else if (e.getIsModifier()) {
1150      return Utilities.removePeriod(defn) + " "+rc.formatPhrase(RenderingI18nContext.SDR_MOD);
1151    } else if (e.getMustSupport()) {
1152      return Utilities.removePeriod(defn) + " "+rc.formatPhrase(RenderingI18nContext.SDR_SUPP);
1153    } else {
1154      return Utilities.removePeriod(defn);
1155    }
1156  }   
1157
1158  private String pvt(DataType ed) {
1159    return ed.getTranslation(lang);
1160  }
1161
1162  private String baseUrl(StructureDefinition sd, ElementDefinition ed, String path) throws FHIRException, IOException {
1163    if (!ed.getPath().equals(ed.getBase().getPath())) {
1164      StructureDefinition sdt = context.fetchTypeDefinition(head(ed.getBase().getPath()));
1165      if (sdt != null) {
1166        sd = sdt;
1167      }
1168    }
1169    return sd.getWebPath()+"#";
1170  }
1171
1172  private String head(String path) {
1173    return path.contains(".") ? path.substring(0, path.indexOf(".")) : path;
1174  }
1175
1176  private String[] textForAttribute(StructureDefinition sd, ElementDefinition e) throws FHIRException, IOException {
1177    LineStatus ls = new LineStatus();
1178    XhtmlNode svg = new XhtmlNode(NodeType.Element, "svg"); // this is a dummy
1179    addAttribute(svg.svgG(null), 0, 0, sd, e, "Element.id", ls, 0, 0);
1180    ls.close();
1181    return ls.list.toArray(new String[] {});
1182  }
1183
1184
1185  private String getTypeCodeForElement(List<TypeRefComponent> tl) {
1186    if (tl.isEmpty())
1187      return "??";
1188    if (tl.size() == 1 && !isReference(tl.get(0).getName()))
1189      return tl.get(0).getName();
1190    String t = tl.get(0).getName();
1191    boolean allSame = true;
1192    for (int i = 1; i < tl.size(); i++) {
1193      allSame = t.equals(tl.get(i).getName());
1194    }
1195    if (allSame && t.equals("Reference")) {
1196      return "Reference";
1197    } else if (allSame && t.equals("canonical")) {
1198      return "canonical";
1199    } else if (allSame && t.equals("CodeableReference")) {
1200      return "CodeableReference";
1201    }  else {
1202      boolean allPrimitive = true;
1203      for (TypeRefComponent tr : tl) {
1204        StructureDefinition sd = context.fetchTypeDefinition(tr.getWorkingCode());
1205        if (sd == null || sd.getKind() != StructureDefinitionKind.PRIMITIVETYPE) {
1206          allPrimitive = false;
1207        }
1208      }
1209      if (VersionUtilities.isR4BVer(context.getVersion())) {
1210        return "Element";
1211      } if (allPrimitive) {
1212        return "PrimitiveType";
1213      } else {
1214        return "DataType";
1215      }
1216    }
1217  }
1218
1219  private boolean isReference(String name) {
1220    return name != null && (name.equals("Reference") || name.equals("canonical") || name.equals("CodeableReference"));
1221  }
1222
1223
1224
1225  private boolean isAttribute(StructureDefinition sd, ElementDefinition c) {
1226    return putils.getChildList(sd, c).isEmpty();
1227  }
1228
1229  private double textWidth(String text) {
1230    return text.length() * CHAR_RATIO;
1231  }
1232
1233
1234  private ClassItem drawElement(XhtmlNode svg, List<String> classNames) throws Exception {
1235    if (classNames != null) {
1236      for (String cn : classNames) {
1237        StructureDefinition sd = context.fetchResource(StructureDefinition.class, cn);
1238        if (sd == null) {
1239          sd = cutils.fetchStructureByName(cn);
1240        }
1241        ElementDefinition ed = sd.getSnapshot().getElementFirstRep();
1242
1243        StructureDefinition base = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition());       
1244        ClassItem parent = base == null ? null : classes.get(base.getName());
1245        if (parent == null) {
1246          drawClass(svg, sd, ed, cn, sd.getStandardsStatus(), base);
1247        } else {
1248          links.add(new Link(parent, drawClass(svg, sd, ed, cn, sd.getStandardsStatus(), null), LinkType.SPECIALIZATION, null, null, PointKind.unknown, null, null));
1249        }
1250
1251      }
1252    }
1253    return null;
1254  }
1255
1256  private ClassItem drawClassElement(XhtmlNode svg, StructureDefinition sd) throws FHIRException, IOException {
1257    ElementDefinition ed = sd.getSnapshot().getElementFirstRep();
1258
1259    StructureDefinition base = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition());       
1260    ClassItem parent = base == null ? null : classes.get(base.getName());
1261    if (parent == null) {
1262      drawClass(svg, sd, ed, ed.getName(), sd.getStandardsStatus(), base);
1263    } else {
1264      links.add(new Link(parent, drawClass(svg, sd, ed, sd.getName(), sd.getStandardsStatus(), null), LinkType.SPECIALIZATION, null, null, PointKind.unknown, null, null));
1265    }
1266    return null;
1267  }
1268  
1269  private ClassItem drawClass(XhtmlNode svg, StructureDefinition sd, ElementDefinition ed, String path, StandardsStatus status, StructureDefinition parent) throws FHIRException, IOException {
1270    ClassItem item = classes.get(path);
1271    if (item == null) {
1272      throw new FHIRException("Unable to find a class for "+path+" from "+CommaSeparatedStringBuilder.join(",", classes.keySet()));
1273    }
1274
1275    var g = svg.svgG(null);
1276    g.attribute("id", prefix+"n"+(++nc));
1277    var rect = g.svgRect(null);
1278    rect.attribute("x", Double.toString(item.left));
1279    rect.attribute("y", Double.toString(item.top));
1280    rect.attribute("rx", "4");
1281    rect.attribute("ry", "4");
1282    rect.attribute("width", Double.toString(item.width));
1283    rect.attribute("height", Double.toString(item.height));
1284    rect.attribute("filter", "url(#shadow"+diagramId+")");
1285    String borderColor = item.getMode() == ClassItemMode.SLICE ? SLICE_COLOR : "black";
1286    if (status == null) {
1287      rect.style("fill:"+(status == null ? "#ffffff" : status.getColorSvg())+";stroke:"+borderColor+";stroke-width:1");
1288    } else {
1289      rect.style("fill:"+status.getColorSvg()+";stroke:"+borderColor+";stroke-width:1");
1290    }
1291    rect.attribute("id", prefix+item.getId());
1292
1293    var line = g.svgLine(null);
1294    line.attribute("x1", Double.toString(item.left));
1295    line.attribute("y1", Double.toString(item.top+HEADER_HEIGHT + GAP_HEIGHT*2));
1296    line.attribute("x2", Double.toString(item.left+item.width));
1297    line.attribute("y2", Double.toString(item.top+HEADER_HEIGHT + GAP_HEIGHT*2));
1298    line.style("stroke:dimgrey;stroke-width:1");
1299    line.attribute("id", prefix+"n"+(++nc));
1300
1301    var text = g.svgText(null);
1302    text.attribute("x", Double.toString(item.left + item.width / 2));
1303    text.attribute("y", Double.toString(item.top+HEADER_HEIGHT));
1304    text.attribute("fill", "black");
1305    if (!path.contains(".")) 
1306      text.attribute("class", "diagram-class-title  diagram-class-resource");
1307    else 
1308      text.attribute("class", "diagram-class-title");
1309    if (parent == null) {
1310      text.attribute("id", prefix+"n"+(++nc));
1311      if (!path.contains(".") && sd.getAbstract()) {
1312        text.style("font-style: italic");
1313      }
1314
1315      var a = text.svgAx(makeLink(item.getName()));
1316      a.attribute("id", prefix+"n"+(++nc));
1317      a.tx(item.getName());
1318
1319      //      if (definitions.getBaseResources().containsKey(e.getName()) && definitions.getBaseResources().get(e.getName()).isInterface()) {
1320      //        xml.text(" ");
1321      //        xml.attribute("xlink:href", "uml.html#interface");
1322      //        xml.enter("a");
1323      //        xml.text("ĞInterfaceğ");
1324      //        xml.exit("a");
1325      //      }
1326    } else if (!path.contains(".")) {
1327      text.attribute("id", prefix+"n"+(++nc));
1328      if (!path.contains(".") && sd.getAbstract()) {
1329        text.style("font-style: italic");
1330      }
1331      var a = text.svgAx(sd.getWebPath());
1332      a.tx(item.getName());
1333      text.tx(" (");
1334      a = text.svgAx(parent.getWebPath());
1335      a.attribute("class", "diagram-class-reference");
1336      a.attribute("id", prefix+"n"+(++nc));
1337      a.style("font-style: italic");
1338      a.tx(parent.getName());
1339      text.tx(")");
1340    } else {
1341      text.attribute("id", prefix+"n"+(++nc));
1342      text.tx(item.getName());
1343    }
1344
1345    List<ElementDefinition> children = putils.getChildList(sd, ed);
1346    if (attributes) {
1347      int i = 0;
1348      for (ElementDefinition c : children) {
1349        if (inScope(c) && countsAsAttribute(sd, c)) {
1350          i++;
1351          addAttribute(g, item.left, item.top+HEADER_HEIGHT + GAP_HEIGHT*2 + LINE_HEIGHT * i, sd, c, path, LINE_HEIGHT, item.width);
1352          String[] texts = textForAttribute(sd, c);
1353          i = i + texts.length - 1;
1354        }
1355      }
1356    }
1357
1358
1359    if (innerClasses) {
1360      for (ElementDefinition c : children) {
1361        if (inScope(c) && countsAsRelationship(sd, c)) {
1362          if (!c.hasSliceName()) {
1363            if (c.hasContentReference()) {
1364              String cr = c.getContentReference();
1365              ClassItem target = classes.get(cr.substring(cr.indexOf("#")+1));
1366              links.add(new Link(item, target, LinkType.COMPOSITION, c.getName(), describeCardinality(c), PointKind.unknown, baseUrl(sd, c, path)+path+"."+c.getName(), getEnhancedDefinition(c)));
1367            } else { 
1368              ClassItem cc = drawClass(svg, sd, c, path+"."+c.getName(), status, null);
1369              links.add(new Link(item, cc, LinkType.COMPOSITION, c.getName(), describeCardinality(c), PointKind.unknown, baseUrl(sd, c, path)+path+"."+c.getName(), getEnhancedDefinition(c)));
1370              if (c.hasSlicing()) {
1371                List<ElementDefinition> slices = getSlices(children, c);
1372                for (ElementDefinition s : slices) {
1373                  ClassItem cc1 = drawClass(svg, sd, s, path+"."+c.getName()+":"+s.getSliceName(), status, null);
1374                  links.add(new Link(cc, cc1, LinkType.SLICE, "", describeCardinality(s), PointKind.unknown, baseUrl(sd, s, path)+path+"."+c.getName()+":"+s.getSliceName(), getEnhancedDefinition(c)));
1375                }
1376              }
1377            }
1378          }
1379        }
1380      }
1381    }
1382    return item;  
1383  }
1384
1385  private void drawLink(XhtmlNode svg, Link l, XhtmlNode insertionPoint) throws FHIRException, IOException {
1386    Point start;
1387    Point end;
1388    Point p1;
1389    Point p2;
1390    String id = l.source.id + "-" + l.target.id;
1391    if (l.source == l.target) {
1392      start = new Point(l.source.right(), l.source.centerV() - SELF_LINK_HEIGHT, PointKind.unknown);
1393      end = new Point(l.source.right(), l.source.centerV() + SELF_LINK_HEIGHT, PointKind.right);
1394      p1 = new Point(l.source.right() + SELF_LINK_WIDTH, l.source.centerV() - SELF_LINK_HEIGHT, PointKind.unknown);
1395      p2 = new Point(l.source.right() + SELF_LINK_WIDTH, l.source.centerV() + SELF_LINK_HEIGHT, PointKind.unknown);
1396
1397      var path = svg.svgPath(insertionPoint);
1398      path.attribute("id", prefix+id);
1399      path.attribute("d", checkForLinkPathData(id, "M"+start.x+" "+start.y+" L"+p1.x+" "+p1.y+" L"+p1.x+" "+p1.y+" L"+p2.x+" "+p2.y+" L"+end.x+" "+end.y));
1400      path.style("stroke:navy;stroke-width:1;fill:none");
1401
1402    } else {
1403      Point c1 = new Point(l.source.centerH(), l.source.centerV(), PointKind.unknown);
1404      Point c2 = new Point(l.target.centerH(), l.target.centerV(), PointKind.unknown);
1405
1406      start = intersection(c1, c2, l.source);
1407      end = intersection(c1, c2, l.target);
1408      if (l.count > 0) {
1409        start.x = adjustForDuplicateX(start.x, start.kind, l.index);
1410        start.y = adjustForDuplicateY(start.y, start.kind, l.index);
1411        end.x = adjustForDuplicateX(end.x, end.kind, l.index);
1412        end.y = adjustForDuplicateY(end.y, end.kind, l.index);
1413
1414      }
1415      p1 = end;
1416      p2 = start;
1417      if (start != null && end != null) {
1418        var path = svg.svgPath(insertionPoint);
1419        path.attribute("d", checkForLinkPathData(id, "M"+Double.toString(start.x)+" "+Double.toString(start.y)+" L"+Double.toString(end.x)+" "+Double.toString(end.y)));
1420        if (l.type == LinkType.CONSTRAINT)
1421          path.style("stroke:orange;stroke-width:1;fill:none");
1422        else if (l.type == LinkType.SLICE)
1423          path.style("stroke:"+SLICE_COLOR+";stroke-width:2;fill:none");
1424        else
1425          path.style("stroke:navy;stroke-width:1;fill:none");
1426        path.attribute("id", prefix+id);      
1427      }
1428    }
1429
1430    if (start != null && end != null) {
1431      if (l.name == null) {
1432        Point pd1 = calcGenRight(start, p1);
1433        Point pd2 = calcGenLeft(start, p1);
1434        var polygon = svg.svgPolygon(insertionPoint);
1435        polygon.attribute("points", start.toPoint() +" " +pd1.toPoint() +" " +pd2.toPoint() +" " +start.toPoint());
1436        polygon.style("fill:white;stroke:navy;stroke-width:1");
1437        polygon.attribute("transform", "rotate("+getAngle(start, p1)+" "+Double.toString(start.x)+" "+Double.toString(start.y)+")");
1438        polygon.attribute("id", prefix+"n"+(++nc));
1439      } else {
1440        // draw the diamond
1441        if (l.type != LinkType.SLICE) {
1442          Point pd2 = calcDiamondEnd(start, p1);
1443          Point pd1 = calcDiamondRight(start, p1);
1444          Point pd3 = calcDiamondLeft(start, p1);
1445          var polygon = svg.svgPolygon(insertionPoint);
1446          polygon.attribute("points", checkForLinkPoints(id, start.toPoint() +" " +pd1.toPoint() +" " +pd2.toPoint() +" " +pd3.toPoint()+" "+start.toPoint()));
1447          polygon.style("fill:navy;stroke:navy;stroke-width:1");
1448          polygon.attribute("transform", checkForLinkTransform(id, "rotate("+getAngle(start, p1)+" "+Double.toString(start.x)+" "+Double.toString(start.y)+")"));
1449          polygon.attribute("id", prefix+id+"-lpolygon");
1450        }
1451
1452        // draw the name half way along
1453        double x = (int) (p1.x + p2.x) / 2;
1454        double y = (int) (p1.y + p2.y) / 2 + LINE_HEIGHT / 2 + LINE_HEIGHT * l.index;
1455        double w = (int) (textWidth(l.name));    
1456        var g = svg.svgG(insertionPoint);
1457        var rect = g.svgRect(null);
1458        rect.attribute("x", checkForKnownX(id, id+"-lrect", Double.toString(x - w/2)));
1459        rect.attribute("y", checkForKnownY(id, id+"-lrect", Double.toString(y - LINE_HEIGHT)));
1460        rect.attribute("width", Double.toString(w));
1461        rect.attribute("height", Double.toString(LINE_HEIGHT + GAP_HEIGHT));
1462        rect.style("fill:white;stroke:black;stroke-width:0");
1463        rect.attribute("id", prefix+id+"-lrect");
1464        
1465        var text = g.svgText(null);
1466
1467        text.attribute("x", checkForKnownX(id, id+"-lname", Double.toString(x)));
1468        text.attribute("y", checkForKnownY(id, id+"-lname", Double.toString(y - GAP_HEIGHT)));
1469        text.attribute("fill", "black");
1470        text.attribute("class", "diagram-class-linkage");
1471        text.attribute("id", prefix+id+"-lname");
1472        var a = text.svgAx(l.path);
1473        a.attribute("id", prefix+"n"+(++nc));        
1474        a.addTag("title").tx(l.description);
1475        a.tx(l.name);
1476
1477
1478        // draw the cardinality at the terminal end
1479        x = end.x;
1480        y = end.y;
1481        if (end.kind == PointKind.left) {
1482          y = y - GAP_HEIGHT;
1483          x = x - 20;
1484        } else if (end.kind == PointKind.top)
1485          y = y - GAP_HEIGHT;
1486        else if (end.kind == PointKind.right) {
1487          y = y - GAP_HEIGHT;
1488          x = x + 15;
1489        } else if (end.kind == PointKind.bottom) 
1490          y = y + LINE_HEIGHT;
1491        w = 18;        
1492        text = svg.svgText(insertionPoint);
1493        text.attribute("x", checkForKnownX(id, id+"-lcard", Double.toString(x)));
1494        text.attribute("y", checkForKnownY(id, id+"-lcard", Double.toString(y)));
1495        text.attribute("fill", "black");
1496        text.attribute("class", "diagram-class-linkage");
1497        text.attribute("id", prefix+id+"-lcard");
1498        text.tx("["+l.cardinality+"]");
1499      }
1500    }
1501  }
1502
1503  private String checkForKnownY(String baseId, String id, String defaultValue) {
1504    if (linkLayouts.containsKey(baseId) && linkLayouts.get(baseId).use && layout.containsKey(id)) {
1505      return ""+layout.get(id).y;
1506    } else {
1507      return defaultValue;
1508    }
1509  }
1510
1511  private String checkForKnownX(String baseId, String id, String defaultValue) {
1512    if (linkLayouts.containsKey(baseId) && linkLayouts.get(baseId).use && layout.containsKey(id)) {
1513      return ""+layout.get(id).x;
1514    } else {
1515      return defaultValue;
1516    }
1517  }
1518
1519  private String checkForLinkTransform(String id, String defaultValue) {
1520    if (linkLayouts.containsKey(id) && linkLayouts.get(id).use && linkLayouts.get(id).diamondTransform != null) {
1521      return linkLayouts.get(id).diamondTransform;
1522    } else {
1523      return defaultValue;
1524    }
1525  }
1526
1527  private String checkForLinkPoints(String id, String defaultValue) {
1528    if (linkLayouts.containsKey(id) && linkLayouts.get(id).use && linkLayouts.get(id).diamondPoints != null) {
1529      return linkLayouts.get(id).diamondPoints;
1530    } else {
1531      return defaultValue;
1532    }
1533  }
1534
1535  private String checkForLinkPathData(String id, String defaultValue) {
1536    if (linkLayouts.containsKey(id) && linkLayouts.get(id).use && linkLayouts.get(id).pathData != null) {
1537      return linkLayouts.get(id).pathData;
1538    } else {
1539      return defaultValue;
1540    }
1541  }
1542
1543  private String abs(double d) {
1544    if (d < 0) {
1545      return Double.toString(-d);
1546    } else {
1547      return Double.toString(d);
1548    }
1549  }
1550
1551  private double adjustForDuplicateX(double x, PointKind kind, int index) {
1552    switch (kind) {
1553    case bottom: 
1554      return x + (DUPLICATE_GAP * (index - 0.5));
1555    case top:
1556      return x + (DUPLICATE_GAP * (index - 0.5));
1557    default:
1558      return x;        
1559    }
1560  }
1561
1562  private double adjustForDuplicateY(double y, PointKind kind, int index) {
1563    switch (kind) {
1564    case left: 
1565      return y - (DUPLICATE_GAP * (index - 0.5));
1566    case right:
1567      return y - (DUPLICATE_GAP * (index - 0.5));
1568    default:
1569      return y;        
1570    }
1571  }
1572
1573  private String getAngle(Point start, Point end) {
1574    double inRads = Math.atan2(end.y - start.y, end.x-start.x);
1575    //    if (inRads < 0)
1576    //      inRads = Math.abs(inRads);
1577    //  else
1578    //      inRads = 2*Math.PI - inRads;
1579
1580    return Double.toString(Math.toDegrees(inRads));
1581  }
1582
1583  private Point calcDiamondEnd(Point start, Point end) {
1584    return new Point(start.x+12, start.y+0, PointKind.unknown);
1585  }
1586
1587  private Point calcDiamondRight(Point start, Point end) {
1588    return new Point(start.x+6, start.y+4, PointKind.unknown);
1589  }
1590
1591  private Point calcDiamondLeft(Point start, Point end) {
1592    return new Point(start.x+6, start.y-4, PointKind.unknown);
1593  }
1594
1595  private Point calcGenRight(Point start, Point end) {
1596    return new Point(start.x+8, start.y+6, PointKind.unknown);
1597  }
1598
1599  private Point calcGenLeft(Point start, Point end) {
1600    return new Point(start.x+8, start.y-6, PointKind.unknown);
1601  }
1602
1603  private Point intersection(Point start, Point end, ClassItem box) {
1604    Point p = calculateIntersect(start.x, start.y, end.x, end.y, box.left, box.top, box.left + box.width, box.top, PointKind.top);
1605    if (p == null)
1606      p = calculateIntersect(start.x, start.y, end.x, end.y, box.left, box.top+box.height, box.left+box.width, box.top+box.height, PointKind.bottom);
1607    if (p == null)
1608      p = calculateIntersect(start.x, start.y, end.x, end.y, box.left, box.top, box.left, box.top+box.height, PointKind.left);
1609    if (p == null)
1610      p = calculateIntersect(start.x, start.y, end.x, end.y, box.left+box.width, box.top, box.left+box.width, box.top+box.height, PointKind.right);
1611    return p;
1612  }
1613
1614  private Point calculateIntersect(double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4, PointKind kind) {
1615    Segment s1 = new Segment(new Point(x1,y1, PointKind.unknown),  new Point(x2,y2, PointKind.unknown));
1616    Segment s2 = new Segment(new Point(x3,y3, PointKind.unknown),  new Point(x4,y4, PointKind.unknown));
1617    return hasIntersection(s1, s2, kind);
1618    //    double slope1 = (y2-y1) / (x2-x1);
1619    //    double slope2 = (y4-y3) / (x4-x3);
1620    //
1621    //    if (Math.abs(slope1 - slope2) < 0.000001)
1622    //      return null;
1623    //    
1624    //    double x = ( ( (x4*y3 - y4*x3) / (x4-x3) ) - ( (x2-y1 - y2*x1) / (x2-x1) ) ) / ( slope1 - slope2 );
1625    //    double y = slope1 * x + ( (x2*y1 - y2*x1) / (x2-x1) );
1626    //    
1627    //    if (inBounds(x, x1, x2) && inBounds(x, x3, x4) && inBounds(y, y1, y2) && inBounds(y, y3, y4))
1628    //      return new Point((int) x, (int) y);
1629    //    else
1630    //      return null;
1631  }
1632
1633  public Point hasIntersection(Segment segment1, Segment segment2, PointKind kind){
1634
1635    if (segment1.isVertical){
1636      if (segment2.isVertical) // ( (segment2.start.x - segment1.start.x)*(segment2.end.x - segment1.start.x) > 0 )
1637        return null;
1638      else {
1639        double fx_at_segment1startx = segment2.slope * segment1.start.x - segment2.intercept;
1640        if (inBounds(fx_at_segment1startx, segment1.start.y, segment1.end.y) && inBounds(segment1.start.x, segment2.start.x, segment2.end.x))
1641          return new Point(segment1.start.x, fx_at_segment1startx, kind);
1642        else
1643          return null;
1644      }
1645    }
1646    else if (segment2.isVertical){
1647      return hasIntersection(segment2, segment1, kind);
1648    }
1649    else { //both segment1 and segment2 are not vertical 
1650      if (segment1.slope == segment2.slope)
1651        return null;
1652      else {
1653        double x1 = segment1.start.x;
1654        double y1 = segment1.start.y;
1655        double x2 = segment1.end.x;
1656        double y2 = segment1.end.y;
1657        double x3 = segment2.start.x;
1658        double y3 = segment2.start.y;
1659        double x4 = segment2.end.x;
1660        double y4 = segment2.end.y;
1661        double x = ((x4*y3-y4*x3)/(x4-x3) - (x2*y1-y2*x1)/(x2-x1)) /( (y2-y1)/(x2-x1) - (y4-y3)/(x4-x3));
1662
1663        if (inBounds(x, x1, x2) && inBounds(x, x3, x4)) { 
1664          return new Point(x, (segment1.slope * x - segment1.intercept), kind);
1665        } else
1666          return null; 
1667      } 
1668    }
1669  }
1670
1671  public String buildConstraintDiagram(StructureDefinition profile, String defnFile) throws FHIRException, IOException {
1672    this.defnFile = defnFile;
1673    this.defnSD = profile;
1674    File f = new File(Utilities.path(sourceFolder, diagramId+".svg"));
1675    if (f.exists()) {
1676      parseSvgFile(f, f.getAbsolutePath());
1677    }
1678    attributes = true;
1679    innerClasses = true;
1680    constraintMode = true;    
1681    classNames.add(profile.getName());
1682
1683
1684    XhtmlNode doc = new XhtmlNode(NodeType.Element, "div");
1685    XhtmlNode svg = doc.svg();
1686    
1687    minx = 0;
1688    miny = 0;
1689
1690    StructureDefinition sd = new SnapshotGenerationPreProcessor(putils).trimSnapshot(profile);
1691    
1692    Point size = determineConstraintMetrics(sd);
1693    adjustAllForMin(size);
1694
1695    svg.attribute("id", prefix+"n"+(++nc));
1696    svg.attribute("version", "1.1");
1697    svg.attribute("width", Double.toString(size.x));
1698    svg.attribute("height", Double.toString(size.y));
1699
1700    shadowFilter(svg);
1701    drawConstraintElement(svg, sd);
1702    countDuplicateLinks();
1703    XhtmlNode insertionPoint = svg.getChildNodes().get(0);
1704    for (Link l : links) {
1705      drawLink(svg, l, insertionPoint);
1706    }
1707
1708
1709    String s = new XhtmlComposer(true, true).compose(doc.getChildNodes());
1710    produceOutput("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+s);
1711    return s; 
1712  }
1713
1714  private void drawConstraintElement(XhtmlNode svg, StructureDefinition sd) throws FHIRException, IOException {
1715
1716    ElementDefinition ed = sd.getSnapshot().getElementFirstRep();
1717
1718    StructureDefinition base = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition());       
1719    drawClass(svg, sd, ed, ed.getPath(), sd.getStandardsStatus(), base);
1720  }
1721
1722  private Point determineConstraintMetrics(StructureDefinition sd) throws FHIRException, IOException {
1723
1724    double width = textWidth("Element") * 1.8;
1725    double height = HEADER_HEIGHT + GAP_HEIGHT*2;
1726    //    if ("true".equals(ini.getStringProperty("diagram", "element-attributes"))) {
1727    //      height = height + LINE_HEIGHT + GAP_HEIGHT;
1728    //      width = textWidth("extension : Extension 0..*");
1729    //    }
1730
1731    Point p = new Point(0, 0, PointKind.unknown);
1732    ClassItem item = new ClassItem(p.x, p.y, width, height, diagramId, null, ClassItemMode.NORMAL);
1733    classes.put(null, item);
1734    double x = item.right()+MARGIN_X;
1735    double y = item.bottom()+MARGIN_Y;
1736    ElementDefinition ed = sd.getSnapshot().getElementFirstRep();
1737    StructureDefinition base = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition());
1738    p = determineMetrics(sd, ed, item, ed.getName(), null, base, ClassItemMode.NORMAL);
1739    x = Math.max(x, p.x+MARGIN_X);
1740    y = Math.max(y, p.y+MARGIN_Y);
1741    return new Point(x, y, PointKind.unknown);
1742  }
1743
1744}