
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}