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