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