001package org.hl7.fhir.r5.renderers; 002 003import java.io.IOException; 004import java.util.ArrayList; 005import java.util.Collections; 006import java.util.Comparator; 007import java.util.HashMap; 008import java.util.HashSet; 009import java.util.List; 010import java.util.Map; 011 012import org.hl7.fhir.exceptions.DefinitionException; 013import org.hl7.fhir.exceptions.FHIRFormatError; 014import org.hl7.fhir.r5.model.CodeSystem; 015import org.hl7.fhir.r5.model.Coding; 016import org.hl7.fhir.r5.model.ConceptMap; 017import org.hl7.fhir.r5.model.ConceptMap.ConceptMapGroupComponent; 018import org.hl7.fhir.r5.model.ConceptMap.ConceptMapGroupUnmappedMode; 019import org.hl7.fhir.r5.model.ConceptMap.MappingPropertyComponent; 020import org.hl7.fhir.r5.model.ConceptMap.OtherElementComponent; 021import org.hl7.fhir.r5.model.ConceptMap.SourceElementComponent; 022import org.hl7.fhir.r5.model.ConceptMap.TargetElementComponent; 023import org.hl7.fhir.r5.model.ContactDetail; 024import org.hl7.fhir.r5.model.ContactPoint; 025import org.hl7.fhir.r5.model.Enumerations.ConceptMapRelationship; 026import org.hl7.fhir.r5.model.Resource; 027import org.hl7.fhir.r5.renderers.ConceptMapRenderer.CollateralDefinition; 028import org.hl7.fhir.r5.renderers.utils.RenderingContext; 029import org.hl7.fhir.r5.renderers.utils.Resolver.ResourceContext; 030import org.hl7.fhir.r5.utils.ToolingExtensions; 031import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 032import org.hl7.fhir.utilities.Utilities; 033import org.hl7.fhir.utilities.xhtml.NodeType; 034import org.hl7.fhir.utilities.xhtml.XhtmlNode; 035 036public class ConceptMapRenderer extends TerminologyRenderer { 037 038 public static class CollateralDefinition { 039 private Resource resource; 040 private String label; 041 public CollateralDefinition(Resource resource, String label) { 042 super(); 043 this.resource = resource; 044 this.label = label; 045 } 046 public Resource getResource() { 047 return resource; 048 } 049 public String getLabel() { 050 return label; 051 } 052 } 053 054 public enum RenderMultiRowSortPolicy { 055 UNSORTED, FIRST_COL, LAST_COL 056 } 057 058 public interface IMultiMapRendererAdvisor { 059 public RenderMultiRowSortPolicy sortPolicy(Object rmmContext); 060 public List<Coding> getMembers(Object rmmContext, String uri); 061 public boolean describeMap(Object rmmContext, ConceptMap map, XhtmlNode x); 062 public boolean hasCollateral(Object rmmContext); 063 public List<CollateralDefinition> getCollateral(Object rmmContext, String uri); // URI identifies which column the collateral is for 064 public String getLink(Object rmmContext, String system, String code); 065 public boolean makeMapLinks(); 066 } 067 068 public static class MultipleMappingRowSorter implements Comparator<MultipleMappingRow> { 069 070 private boolean first; 071 072 protected MultipleMappingRowSorter(boolean first) { 073 super(); 074 this.first = first; 075 } 076 077 @Override 078 public int compare(MultipleMappingRow o1, MultipleMappingRow o2) { 079 String s1 = first ? o1.firstCode() : o1.lastCode(); 080 String s2 = first ? o2.firstCode() : o2.lastCode(); 081 return s1.compareTo(s2); 082 } 083 } 084 085 public static class Cell { 086 087 private String system; 088 private String code; 089 private String display; 090 private String relationship; 091 private String relComment; 092 public boolean renderedRel; 093 public boolean renderedCode; 094 private Cell clone; 095 096 protected Cell() { 097 super(); 098 } 099 100 public Cell(String system, String code, String display) { 101 this.system = system; 102 this.code = code; 103 this.display = display; 104 } 105 106 public Cell(String system, String code, String relationship, String comment) { 107 this.system = system; 108 this.code = code; 109 this.relationship = relationship; 110 this.relComment = comment; 111 } 112 113 public boolean matches(String system, String code) { 114 return (system != null && system.equals(this.system)) && (code != null && code.equals(this.code)); 115 } 116 117 public String present() { 118 if (system == null) { 119 return code; 120 } else { 121 return code; //+(clone == null ? "" : " (@"+clone.code+")"); 122 } 123 } 124 125 public Cell copy(boolean clone) { 126 Cell res = new Cell(); 127 res.system = system; 128 res.code = code; 129 res.display = display; 130 res.relationship = relationship; 131 res.relComment = relComment; 132 res.renderedRel = renderedRel; 133 res.renderedCode = renderedCode; 134 if (clone) { 135 res.clone = this; 136 } 137 return res; 138 } 139 140 @Override 141 public String toString() { 142 return relationship+" "+system + "#" + code + " \"" + display + "\""; 143 } 144 145 } 146 147 148 public static class MultipleMappingRowItem { 149 List<Cell> cells = new ArrayList<>(); 150 151 @Override 152 public String toString() { 153 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 154 for (Cell cell : cells) { 155 if (cell.relationship != null) { 156 b.append(cell.relationship+cell.code); 157 } else { 158 b.append(cell.code); 159 } 160 } 161 return b.toString(); 162 } 163 } 164 165 public static class MultipleMappingRow { 166 private List<MultipleMappingRowItem> rowSets = new ArrayList<>(); 167 private MultipleMappingRow stickySource; 168 169 public MultipleMappingRow(int i, String system, String code, String display) { 170 MultipleMappingRowItem row = new MultipleMappingRowItem(); 171 rowSets.add(row); 172 for (int c = 0; c < i; c++) { 173 row.cells.add(new Cell()); // blank cell spaces 174 } 175 row.cells.add(new Cell(system, code, display)); 176 } 177 178 179 public MultipleMappingRow(MultipleMappingRow stickySource) { 180 this.stickySource = stickySource; 181 } 182 183 @Override 184 public String toString() { 185 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 186 for (MultipleMappingRowItem rowSet : rowSets) { 187 b.append(""+rowSet.cells.size()); 188 } 189 CommaSeparatedStringBuilder b2 = new CommaSeparatedStringBuilder(";"); 190 for (MultipleMappingRowItem rowSet : rowSets) { 191 b2.append(rowSet.toString()); 192 } 193 return ""+rowSets.size()+" ["+b.toString()+"] ("+b2.toString()+")"; 194 } 195 196 197 public String lastCode() { 198 MultipleMappingRowItem first = rowSets.get(0); 199 for (int i = first.cells.size()-1; i >= 0; i--) { 200 if (first.cells.get(i).code != null) { 201 return first.cells.get(i).code; 202 } 203 } 204 return ""; 205 } 206 207 public String firstCode() { 208 MultipleMappingRowItem first = rowSets.get(0); 209 for (int i = 0; i < first.cells.size(); i++) { 210 if (first.cells.get(i).code != null) { 211 return first.cells.get(i).code; 212 } 213 } 214 return ""; 215 } 216 217 public void addSource(MultipleMappingRow sourceRow, List<MultipleMappingRow> rowList, ConceptMapRelationship relationship, String comment) { 218 // we already have a row, and we're going to collapse the rows on sourceRow into here, and add a matching terminus 219 assert sourceRow.rowSets.get(0).cells.size() == rowSets.get(0).cells.size()-1; 220 rowList.remove(sourceRow); 221 Cell template = rowSets.get(0).cells.get(rowSets.get(0).cells.size()-1); 222 for (MultipleMappingRowItem row : sourceRow.rowSets) { 223 row.cells.add(new Cell(template.system, template.code, relationship.getSymbol(), comment)); 224 } 225 rowSets.addAll(sourceRow.rowSets); 226 } 227 228 public void addTerminus() { 229 for (MultipleMappingRowItem row : rowSets) { 230 row.cells.add(new Cell(null, null, "X", null)); 231 } 232 } 233 234 public void addTarget(String system, String code, ConceptMapRelationship relationship, String comment, List<MultipleMappingRow> sets, int colCount) { 235 if (rowSets.get(0).cells.size() == colCount+1) { // if it's already has a target for this col then we have to clone (and split) the rows 236 for (MultipleMappingRowItem row : rowSets) { 237 row.cells.add(new Cell(system, code, relationship.getSymbol(), comment)); 238 } 239 } else { 240 MultipleMappingRow nrow = new MultipleMappingRow(this); 241 for (MultipleMappingRowItem row : rowSets) { 242 MultipleMappingRowItem n = new MultipleMappingRowItem(); 243 for (int i = 0; i < row.cells.size()-1; i++) { // note to skip the last 244 n.cells.add(row.cells.get(i).copy(true)); 245 } 246 n.cells.add(new Cell(system, code, relationship.getSymbol(), comment)); 247 nrow.rowSets.add(n); 248 } 249 sets.add(sets.indexOf(this), nrow); 250 } 251 } 252 253 public String lastSystem() { 254 MultipleMappingRowItem first = rowSets.get(0); 255 for (int i = first.cells.size()-1; i >= 0; i--) { 256 if (first.cells.get(i).system != null) { 257 return first.cells.get(i).system; 258 } 259 } 260 return ""; 261 } 262 263 public void addCopy(String system) { 264 for (MultipleMappingRowItem row : rowSets) { 265 row.cells.add(new Cell(system, lastCode(), "=", null)); 266 } 267 } 268 269 270 public boolean alreadyHasMappings(int i) { 271 for (MultipleMappingRowItem row : rowSets) { 272 if (row.cells.size() > i+1) { 273 return true; 274 } 275 } 276 return false; 277 } 278 279 280 public Cell getLastSource(int i) { 281 for (MultipleMappingRowItem row : rowSets) { 282 return row.cells.get(i+1); 283 } 284 throw new Error("Should not get here"); // return null 285 } 286 287 288 public void cloneSource(int i, Cell cell) { 289 MultipleMappingRowItem row = new MultipleMappingRowItem(); 290 rowSets.add(row); 291 for (int c = 0; c < i-1; c++) { 292 row.cells.add(new Cell()); // blank cell spaces 293 } 294 row.cells.add(cell.copy(true)); 295 row.cells.add(rowSets.get(0).cells.get(rowSets.get(0).cells.size()-1).copy(false)); 296 } 297 } 298 299 public ConceptMapRenderer(RenderingContext context) { 300 super(context); 301 } 302 303 public ConceptMapRenderer(RenderingContext context, ResourceContext rcontext) { 304 super(context, rcontext); 305 } 306 307 public boolean render(XhtmlNode x, Resource dr) throws FHIRFormatError, DefinitionException, IOException { 308 return render(x, (ConceptMap) dr, false); 309 } 310 311 public boolean render(XhtmlNode x, ConceptMap cm, boolean header) throws FHIRFormatError, DefinitionException, IOException { 312 if (header) { 313 x.h2().addText(cm.getName()+" ("+cm.getUrl()+")"); 314 } 315 316 XhtmlNode p = x.para(); 317 p.tx(context.formatPhrase(RenderingContext.CONC_MAP_FROM) + " "); 318 if (cm.hasSourceScope()) 319 AddVsRef(cm.getSourceScope().primitiveValue(), p, cm); 320 else 321 p.tx(context.formatPhrase(RenderingContext.CONC_MAP_NOT_SPEC)); 322 p.tx(" "+ (context.formatPhrase(RenderingContext.CONC_MAP_TO) + " ")); 323 if (cm.hasTargetScope()) 324 AddVsRef(cm.getTargetScope().primitiveValue(), p, cm); 325 else 326 p.tx(context.formatPhrase(RenderingContext.CONC_MAP_NOT_SPEC)); 327 328 p = x.para(); 329 if (cm.getExperimental()) 330 p.addText(Utilities.capitalize(cm.getStatus().toString())+" "+ (context.formatPhrase(RenderingContext.CONC_MAP_NO_PROD_USE) + " ")); 331 else 332 p.addText(Utilities.capitalize(cm.getStatus().toString())+". "); 333 p.tx(context.formatPhrase(RenderingContext.CONC_MAP_PUB_ON, (cm.hasDate() ? display(cm.getDateElement()) : "?ngen-10?")+" by "+cm.getPublisher()) + " "); 334 if (!cm.getContact().isEmpty()) { 335 p.tx(" ("); 336 boolean firsti = true; 337 for (ContactDetail ci : cm.getContact()) { 338 if (firsti) 339 firsti = false; 340 else 341 p.tx(", "); 342 if (ci.hasName()) 343 p.addText(ci.getName()+": "); 344 boolean first = true; 345 for (ContactPoint c : ci.getTelecom()) { 346 if (first) 347 first = false; 348 else 349 p.tx(", "); 350 addTelecom(p, c); 351 } 352 } 353 p.tx(")"); 354 } 355 p.tx(". "); 356 p.addText(cm.getCopyright()); 357 if (!Utilities.noString(cm.getDescription())) 358 addMarkdown(x, cm.getDescription()); 359 360 x.br(); 361 int gc = 0; 362 363 CodeSystem cs = getContext().getWorker().fetchCodeSystem("http://hl7.org/fhir/concept-map-relationship"); 364 if (cs == null) 365 cs = getContext().getWorker().fetchCodeSystem("http://hl7.org/fhir/concept-map-equivalence"); 366 String eqpath = cs == null ? null : cs.getWebPath(); 367 368 for (ConceptMapGroupComponent grp : cm.getGroup()) { 369 String src = grp.getSource(); 370 boolean comment = false; 371 boolean ok = true; 372 Map<String, HashSet<String>> props = new HashMap<String, HashSet<String>>(); 373 Map<String, HashSet<String>> sources = new HashMap<String, HashSet<String>>(); 374 Map<String, HashSet<String>> targets = new HashMap<String, HashSet<String>>(); 375 sources.put("code", new HashSet<String>()); 376 targets.put("code", new HashSet<String>()); 377 sources.get("code").add(grp.getSource()); 378 targets.get("code").add(grp.getTarget()); 379 for (SourceElementComponent ccl : grp.getElement()) { 380 ok = ok && (ccl.getNoMap() || (ccl.getTarget().size() == 1 && ccl.getTarget().get(0).getDependsOn().isEmpty() && ccl.getTarget().get(0).getProduct().isEmpty())); 381 for (TargetElementComponent ccm : ccl.getTarget()) { 382 comment = comment || !Utilities.noString(ccm.getComment()); 383 for (MappingPropertyComponent pp : ccm.getProperty()) { 384 if (!props.containsKey(pp.getCode())) 385 props.put(pp.getCode(), new HashSet<String>()); 386 } 387 for (OtherElementComponent d : ccm.getDependsOn()) { 388 if (!sources.containsKey(d.getAttribute())) 389 sources.put(d.getAttribute(), new HashSet<String>()); 390 } 391 for (OtherElementComponent d : ccm.getProduct()) { 392 if (!targets.containsKey(d.getAttribute())) 393 targets.put(d.getAttribute(), new HashSet<String>()); 394 } 395 } 396 } 397 398 gc++; 399 if (gc > 1) { 400 x.hr(); 401 } 402 XhtmlNode pp = x.para(); 403 pp.b().tx(context.formatPhrase(RenderingContext.CONC_MAP_GRP, gc) + " "); 404 pp.tx(context.formatPhrase(RenderingContext.CONC_MAP_FROM) + " "); 405 if (grp.hasSource()) { 406 renderCanonical(cm, pp, grp.getSource()); 407 } else { 408 pp.code(context.formatPhrase(RenderingContext.CONC_MAP_CODE_SYS_UNSPEC)); 409 } 410 pp.tx(" to "); 411 if (grp.hasTarget()) { 412 renderCanonical(cm, pp, grp.getTarget()); 413 } else { 414 pp.code(context.formatPhrase(RenderingContext.CONC_MAP_CODE_SYS_UNSPEC)); 415 } 416 417 String display; 418 if (ok) { 419 // simple 420 XhtmlNode tbl = x.table( "grid"); 421 XhtmlNode tr = tbl.tr(); 422 tr.td().b().tx(context.formatPhrase(RenderingContext.CONC_MAP_SOURCE)); 423 tr.td().b().tx(context.formatPhrase(RenderingContext.CONC_MAP_REL)); 424 tr.td().b().tx(context.formatPhrase(RenderingContext.CONC_MAP_TRGT)); 425 if (comment) 426 tr.td().b().tx(context.formatPhrase(RenderingContext.GENERAL_COMMENT)); 427 for (SourceElementComponent ccl : grp.getElement()) { 428 tr = tbl.tr(); 429 XhtmlNode td = tr.td(); 430 td.addText(ccl.getCode()); 431 display = ccl.hasDisplay() ? ccl.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getSource()), versionFromCanonical(grp.getSource()), ccl.getCode()); 432 if (display != null && !isSameCodeAndDisplay(ccl.getCode(), display)) 433 td.tx(" ("+display+")"); 434 if (ccl.getNoMap()) { 435 tr.td().colspan(comment ? "3" : "2").style("background-color: #efefef").tx("(not mapped)"); 436 } else { 437 TargetElementComponent ccm = ccl.getTarget().get(0); 438 if (!ccm.hasRelationship()) 439 tr.td().tx(":"+"("+ConceptMapRelationship.EQUIVALENT.toCode()+")"); 440 else { 441 if (ccm.hasExtension(ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE)) { 442 String code = ToolingExtensions.readStringExtension(ccm, ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE); 443 tr.td().ah(eqpath+"#"+code, code).tx(presentEquivalenceCode(code)); 444 } else { 445 tr.td().ah(eqpath+"#"+ccm.getRelationship().toCode(), ccm.getRelationship().toCode()).tx(presentRelationshipCode(ccm.getRelationship().toCode())); 446 } 447 } 448 td = tr.td(); 449 td.addText(ccm.getCode()); 450 display = ccm.hasDisplay() ? ccm.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getTarget()), versionFromCanonical(grp.getTarget()), ccm.getCode()); 451 if (display != null && !isSameCodeAndDisplay(ccm.getCode(), display)) 452 td.tx(" ("+display+")"); 453 if (comment) 454 tr.td().addText(ccm.getComment()); 455 } 456 addUnmapped(tbl, grp); 457 } 458 } else { 459 boolean hasRelationships = false; 460 for (int si = 0; si < grp.getElement().size(); si++) { 461 SourceElementComponent ccl = grp.getElement().get(si); 462 for (int ti = 0; ti < ccl.getTarget().size(); ti++) { 463 TargetElementComponent ccm = ccl.getTarget().get(ti); 464 if (ccm.hasRelationship()) { 465 hasRelationships = true; 466 } 467 } 468 } 469 470 XhtmlNode tbl = x.table( "grid"); 471 XhtmlNode tr = tbl.tr(); 472 XhtmlNode td; 473 tr.td().colspan(Integer.toString(1+sources.size())).b().tx(context.formatPhrase(RenderingContext.CONC_MAP_SRC_DET)); 474 if (hasRelationships) { 475 tr.td().b().tx(context.formatPhrase(RenderingContext.CONC_MAP_REL)); 476 } 477 tr.td().colspan(Integer.toString(1+targets.size())).b().tx(context.formatPhrase(RenderingContext.CONC_MAP_TRGT_DET)); 478 if (comment) { 479 tr.td().b().tx(context.formatPhrase(RenderingContext.GENERAL_COMMENT)); 480 } 481 tr.td().colspan(Integer.toString(1+targets.size())).b().tx(context.formatPhrase(RenderingContext.GENERAL_PROPS)); 482 tr = tbl.tr(); 483 if (sources.get("code").size() == 1) { 484 String url = sources.get("code").iterator().next(); 485 renderCSDetailsLink(tr, url, true); 486 } else 487 tr.td().b().tx(context.formatPhrase(RenderingContext.GENERAL_CODE)); 488 for (String s : sources.keySet()) { 489 if (s != null && !s.equals("code")) { 490 if (sources.get(s).size() == 1) { 491 String url = sources.get(s).iterator().next(); 492 renderCSDetailsLink(tr, url, false); 493 } else 494 tr.td().b().addText(getDescForConcept(s)); 495 } 496 } 497 if (hasRelationships) { 498 tr.td(); 499 } 500 if (targets.get("code").size() == 1) { 501 String url = targets.get("code").iterator().next(); 502 renderCSDetailsLink(tr, url, true); 503 } else 504 tr.td().b().tx(context.formatPhrase(RenderingContext.GENERAL_CODE)); 505 for (String s : targets.keySet()) { 506 if (s != null && !s.equals("code")) { 507 if (targets.get(s).size() == 1) { 508 String url = targets.get(s).iterator().next(); 509 renderCSDetailsLink(tr, url, false); 510 } else 511 tr.td().b().addText(getDescForConcept(s)); 512 } 513 } 514 if (comment) { 515 tr.td(); 516 } 517 for (String s : props.keySet()) { 518 if (s != null) { 519 if (props.get(s).size() == 1) { 520 String url = props.get(s).iterator().next(); 521 renderCSDetailsLink(tr, url, false); 522 } else 523 tr.td().b().addText(getDescForConcept(s)); 524 } 525 } 526 527 for (int si = 0; si < grp.getElement().size(); si++) { 528 SourceElementComponent ccl = grp.getElement().get(si); 529 boolean slast = si == grp.getElement().size()-1; 530 boolean first = true; 531 if (ccl.hasNoMap() && ccl.getNoMap()) { 532 tr = tbl.tr(); 533 td = tr.td().style("border-right-width: 0px"); 534 if (!first) 535 td.style("border-top-style: none"); 536 else 537 td.style("border-bottom-style: none"); 538 if (sources.get("code").size() == 1) 539 td.addText(ccl.getCode()); 540 else 541 td.addText(grp.getSource()+" / "+ccl.getCode()); 542 display = ccl.hasDisplay() ? ccl.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getSource()), versionFromCanonical(grp.getSource()), ccl.getCode()); 543 tr.td().style("border-left-width: 0px").tx(display == null ? "" : display); 544 tr.td().colspan("4").style("background-color: #efefef").tx("(not mapped)"); 545 546 } else { 547 for (int ti = 0; ti < ccl.getTarget().size(); ti++) { 548 TargetElementComponent ccm = ccl.getTarget().get(ti); 549 boolean last = ti == ccl.getTarget().size()-1; 550 tr = tbl.tr(); 551 td = tr.td().style("border-right-width: 0px"); 552 if (!first && !last) 553 td.style("border-top-style: none; border-bottom-style: none"); 554 else if (!first) 555 td.style("border-top-style: none"); 556 else if (!last) 557 td.style("border-bottom-style: none"); 558 if (first) { 559 if (sources.get("code").size() == 1) 560 td.addText(ccl.getCode()); 561 else 562 td.addText(grp.getSource()+" / "+ccl.getCode()); 563 display = ccl.hasDisplay() ? ccl.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getSource()), versionFromCanonical(grp.getSource()), ccl.getCode()); 564 td = tr.td(); 565 if (!last) 566 td.style("border-left-width: 0px; border-bottom-style: none"); 567 else 568 td.style("border-left-width: 0px"); 569 td.tx(display == null ? "" : display); 570 } else { 571 td = tr.td(); // for display 572 if (!last) 573 td.style("border-left-width: 0px; border-top-style: none; border-bottom-style: none"); 574 else 575 td.style("border-top-style: none; border-left-width: 0px"); 576 } 577 for (String s : sources.keySet()) { 578 if (s != null && !s.equals("code")) { 579 td = tr.td(); 580 if (first) { 581 td.addText(getValue(ccm.getDependsOn(), s, sources.get(s).size() != 1)); 582 display = getDisplay(ccm.getDependsOn(), s); 583 if (display != null) 584 td.tx(" ("+display+")"); 585 } 586 } 587 } 588 first = false; 589 if (hasRelationships) { 590 if (!ccm.hasRelationship()) 591 tr.td(); 592 else { 593 if (ccm.getRelationshipElement().hasExtension(ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE)) { 594 String code = ToolingExtensions.readStringExtension(ccm.getRelationshipElement(), ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE); 595 tr.td().ah(eqpath+"#"+code, code).tx(presentEquivalenceCode(code)); 596 } else { 597 tr.td().ah(eqpath+"#"+ccm.getRelationship().toCode(), ccm.getRelationship().toCode()).tx(presentRelationshipCode(ccm.getRelationship().toCode())); 598 } 599 } 600 } 601 td = tr.td().style("border-right-width: 0px"); 602 if (targets.get("code").size() == 1) 603 td.addText(ccm.getCode()); 604 else 605 td.addText(grp.getTarget()+" / "+ccm.getCode()); 606 display = ccm.hasDisplay() ? ccm.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getTarget()), versionFromCanonical(grp.getTarget()), ccm.getCode()); 607 tr.td().style("border-left-width: 0px").tx(display == null ? "" : display); 608 609 for (String s : targets.keySet()) { 610 if (s != null && !s.equals("code")) { 611 td = tr.td(); 612 td.addText(getValue(ccm.getProduct(), s, targets.get(s).size() != 1)); 613 display = getDisplay(ccm.getProduct(), s); 614 if (display != null) 615 td.tx(" ("+display+")"); 616 } 617 } 618 if (comment) 619 tr.td().addText(ccm.getComment()); 620 621 for (String s : props.keySet()) { 622 if (s != null) { 623 td = tr.td(); 624 td.addText(getValue(ccm.getProperty(), s)); 625 } 626 } 627 } 628 } 629 addUnmapped(tbl, grp); 630 } 631 } 632 } 633 return true; 634 } 635 636 public void describe(XhtmlNode x, ConceptMap cm) { 637 x.tx(display(cm)); 638 } 639 640 public String display(ConceptMap cm) { 641 return cm.present(); 642 } 643 644 private boolean isSameCodeAndDisplay(String code, String display) { 645 String c = code.replace(" ", "").replace("-", "").toLowerCase(); 646 String d = display.replace(" ", "").replace("-", "").toLowerCase(); 647 return c.equals(d); 648 } 649 650 651 private String presentRelationshipCode(String code) { 652 if ("related-to".equals(code)) { 653 return "is related to"; 654 } else if ("equivalent".equals(code)) { 655 return "is equivalent to"; 656 } else if ("source-is-narrower-than-target".equals(code)) { 657 return "is narrower than"; 658 } else if ("source-is-broader-than-target".equals(code)) { 659 return "is broader than"; 660 } else if ("not-related-to".equals(code)) { 661 return "is not related to"; 662 } else { 663 return code; 664 } 665 } 666 667 private String presentEquivalenceCode(String code) { 668 if ("relatedto".equals(code)) { 669 return "is related to"; 670 } else if ("equivalent".equals(code)) { 671 return "is equivalent to"; 672 } else if ("equal".equals(code)) { 673 return "is equal to"; 674 } else if ("wider".equals(code)) { 675 return "maps to wider concept"; 676 } else if ("subsumes".equals(code)) { 677 return "is subsumed by"; 678 } else if ("source-is-broader-than-target".equals(code)) { 679 return "maps to narrower concept"; 680 } else if ("specializes".equals(code)) { 681 return "has specialization"; 682 } else if ("inexact".equals(code)) { 683 return "maps loosely to"; 684 } else if ("unmatched".equals(code)) { 685 return "has no match"; 686 } else if ("disjoint".equals(code)) { 687 return "is not related to"; 688 } else { 689 return code; 690 } 691 } 692 693 public void renderCSDetailsLink(XhtmlNode tr, String url, boolean span2) { 694 CodeSystem cs; 695 XhtmlNode td; 696 cs = getContext().getWorker().fetchCodeSystem(url); 697 td = tr.td(); 698 if (span2) { 699 td.colspan("2"); 700 } 701 td.b().tx(context.formatPhrase(RenderingContext.CONC_MAP_CODES)); 702 td.tx(" " + (context.formatPhrase(RenderingContext.CONC_MAP_FRM) + " ")); 703 if (cs == null) 704 td.tx(url); 705 else 706 td.ah(context.fixReference(cs.getWebPath())).attribute("title", url).tx(cs.present()); 707 } 708 709 private void addUnmapped(XhtmlNode tbl, ConceptMapGroupComponent grp) { 710 if (grp.hasUnmapped()) { 711// throw new Error("not done yet"); 712 } 713 714 } 715 716 private String getDescForConcept(String s) { 717 if (s.startsWith("http://hl7.org/fhir/v2/element/")) 718 return "v2 "+s.substring("http://hl7.org/fhir/v2/element/".length()); 719 return s; 720 } 721 722 723 private String getValue(List<MappingPropertyComponent> list, String s) { 724 return "todo"; 725 } 726 727 private String getValue(List<OtherElementComponent> list, String s, boolean withSystem) { 728 for (OtherElementComponent c : list) { 729 if (s.equals(c.getAttribute())) 730 if (withSystem) 731 return /*c.getSystem()+" / "+*/c.getValue().primitiveValue(); 732 else 733 return c.getValue().primitiveValue(); 734 } 735 return null; 736 } 737 738 private String getDisplay(List<OtherElementComponent> list, String s) { 739 for (OtherElementComponent c : list) { 740 if (s.equals(c.getAttribute())) { 741 // return getDisplayForConcept(systemFromCanonical(c.getSystem()), versionFromCanonical(c.getSystem()), c.getValue()); 742 } 743 } 744 return null; 745 } 746 747 public static XhtmlNode renderMultipleMaps(String start, List<ConceptMap> maps, IMultiMapRendererAdvisor advisor, Object rmmContext) { 748 // 1+1 column for each provided map 749 List<MultipleMappingRow> rowSets = new ArrayList<>(); 750 for (int i = 0; i < maps.size(); i++) { 751 populateRows(rowSets, maps.get(i), i, advisor, rmmContext); 752 } 753 collateRows(rowSets); 754 if (advisor.sortPolicy(rmmContext) != RenderMultiRowSortPolicy.UNSORTED) { 755 Collections.sort(rowSets, new MultipleMappingRowSorter(advisor.sortPolicy(rmmContext) == RenderMultiRowSortPolicy.FIRST_COL)); 756 } 757 XhtmlNode div = new XhtmlNode(NodeType.Element, "div"); 758 XhtmlNode tbl = div.table("none").style("text-align: left; border-spacing: 0; padding: 5px"); 759 XhtmlNode tr = tbl.tr(); 760 styleCell(tr.td(), false, true, 5).b().tx(start); 761 for (ConceptMap map : maps) { 762 XhtmlNode td = styleCell(tr.td(), false, true, 5).colspan(2); 763 if (!advisor.describeMap(rmmContext, map, td)) { 764 if (map.hasWebPath() && advisor.makeMapLinks()) { 765 td.b().ah(map.getWebPath(), map.getVersionedUrl()).tx(map.present()); 766 } else { 767 td.b().tx(map.present()); 768 } 769 } 770 } 771 if (advisor.hasCollateral(rmmContext)) { 772 tr = tbl.tr(); 773 renderLinks(styleCell(tr.td(), false, true, 5), advisor.getCollateral(rmmContext, null)); 774 for (ConceptMap map : maps) { 775 renderLinks(styleCell(tr.td(), false, true, 5).colspan(2), advisor.getCollateral(rmmContext, map.getUrl())); 776 } 777 } 778 for (MultipleMappingRow row : rowSets) { 779 renderMultiRow(tbl, row, maps, advisor, rmmContext); 780 } 781 return div; 782 } 783 784 private static void renderLinks(XhtmlNode td, List<CollateralDefinition> collateral) { 785 if (collateral.size() > 0) { 786 td.tx( "Links:"); 787 td.tx(" "); 788 boolean first = true; 789 for (CollateralDefinition c : collateral) { 790 if (first) first = false; else td.tx(", "); 791 td.ah(c.getResource().getWebPath()).tx(c.getLabel()); 792 } 793 } 794 } 795 796 private static void collateRows(List<MultipleMappingRow> rowSets) { 797 List<MultipleMappingRow> toDelete = new ArrayList<ConceptMapRenderer.MultipleMappingRow>(); 798 for (MultipleMappingRow rowSet : rowSets) { 799 MultipleMappingRow tgt = rowSet.stickySource; 800 while (toDelete.contains(tgt)) { 801 tgt = tgt.stickySource; 802 } 803 if (tgt != null && rowSets.contains(tgt)) { 804 tgt.rowSets.addAll(rowSet.rowSets); 805 toDelete.add(rowSet); 806 } 807 } 808 rowSets.removeAll(toDelete); 809 } 810 811 private static void renderMultiRow(XhtmlNode tbl, MultipleMappingRow rows, List<ConceptMap> maps, IMultiMapRendererAdvisor advisor, Object rmmContext) { 812 int rowCounter = 0; 813 for (MultipleMappingRowItem row : rows.rowSets) { 814 XhtmlNode tr = tbl.tr(); 815 boolean first = true; 816 int cellCounter = 0; 817 Cell last = null; 818 for (Cell cell : row.cells) { 819 if (first) { 820 if (!cell.renderedCode) { 821 int c = 1; 822 for (int i = rowCounter + 1; i < rows.rowSets.size(); i++) { 823 if (cell.code != null && rows.rowSets.get(i).cells.size() > cellCounter && cell.code.equals(rows.rowSets.get(i).cells.get(cellCounter).code)) { 824 rows.rowSets.get(i).cells.get(cellCounter).renderedCode = true; 825 c++; 826 } else { 827 break; 828 } 829 } 830 if (cell.code == null) { 831 styleCell(tr.td(), rowCounter == 0, true, 5).rowspan(c).style("background-color: #eeeeee"); 832 } else { 833 String link = advisor.getLink(rmmContext, cell.system, cell.code); 834 XhtmlNode x = null; 835 if (link != null) { 836 x = styleCell(tr.td(), rowCounter == 0, true, 5).attributeNN("title", cell.display).rowspan(c).ah(link); 837 } else { 838 x = styleCell(tr.td(), rowCounter == 0, true, 5).attributeNN("title", cell.display).rowspan(c); 839 } 840// if (cell.clone != null) { 841// x.style("color: grey"); 842// } 843 x.tx(cell.present()); 844 } 845 } 846 first = false; 847 } else { 848 if (!cell.renderedRel) { 849 int c = 1; 850 for (int i = rowCounter + 1; i < rows.rowSets.size(); i++) { 851 if ((cell.relationship != null && rows.rowSets.get(i).cells.size() > cellCounter && cell.relationship.equals(rows.rowSets.get(i).cells.get(cellCounter).relationship)) && 852 (cell.code != null && cell.code.equals(rows.rowSets.get(i).cells.get(cellCounter).code)) && 853 (last.code != null && cell.code.equals(rows.rowSets.get(i).cells.get(cellCounter-1).code))) { 854 rows.rowSets.get(i).cells.get(cellCounter).renderedRel = true; 855 c++; 856 } else { 857 break; 858 } 859 } 860 if (last.code == null || cell.code == null) { 861 styleCell(tr.td(), rowCounter == 0, true, 5).style("background-color: #eeeeee"); 862 } else if (cell.relationship != null) { 863 styleCell(tr.tdW(16), rowCounter == 0, true, 0).attributeNN("title", cell.relComment).rowspan(c).style("background-color: LightGrey; text-align: center; vertical-align: middle; color: white").tx(cell.relationship); 864 } else { 865 styleCell(tr.tdW(16), rowCounter == 0, false, 0).rowspan(c); 866 } 867 } 868 if (!cell.renderedCode) { 869 int c = 1; 870 for (int i = rowCounter + 1; i < rows.rowSets.size(); i++) { 871 if (cell.code != null && rows.rowSets.get(i).cells.size() > cellCounter && cell.code.equals(rows.rowSets.get(i).cells.get(cellCounter).code)) { 872 rows.rowSets.get(i).cells.get(cellCounter).renderedCode = true; 873 c++; 874 } else { 875 break; 876 } 877 } 878 if (cell.code == null) { 879 styleCell(tr.td(), rowCounter == 0, true, 5).rowspan(c).style("background-color: #eeeeee"); 880 } else { 881 String link = advisor.getLink(rmmContext, cell.system, cell.code); 882 XhtmlNode x = null; 883 if (link != null) { 884 x = styleCell(tr.td(), rowCounter == 0, true, 5).attributeNN("title", cell.display).rowspan(c).ah(link); 885 } else { 886 x = styleCell(tr.td(), rowCounter == 0, true, 5).attributeNN("title", cell.display).rowspan(c); 887 } 888// if (cell.clone != null) { 889// x.style("color: grey"); 890// } 891 x.tx(cell.present()); 892 } 893 } 894 } 895 last = cell; 896 cellCounter++; 897 } 898 rowCounter++; 899 } 900 } 901 902 private static XhtmlNode styleCell(XhtmlNode td, boolean firstrow, boolean sides, int padding) { 903 if (firstrow) { 904 td.style("vertical-align: middle; border-top: 1px solid black; padding: "+padding+"px"); 905 } else { 906 td.style("vertical-align: middle; border-top: 1px solid LightGrey; padding: "+padding+"px"); 907 } 908 if (sides) { 909 td.style("border-left: 1px solid LightGrey; border-right: 2px solid LightGrey"); 910 } 911 return td; 912 } 913 914 private static void populateRows(List<MultipleMappingRow> rowSets, ConceptMap map, int i, IMultiMapRendererAdvisor advisor, Object rmmContext) { 915 // if we can resolve the value set, we create entries for it 916 if (map.hasSourceScope()) { 917 List<Coding> codings = advisor.getMembers(rmmContext, map.getSourceScope().primitiveValue()); 918 if (codings != null) { 919 for (Coding c : codings) { 920 MultipleMappingRow row = i == 0 ? null : findExistingRowBySource(rowSets, c.getSystem(), c.getCode(), i); 921 if (row == null) { 922 row = new MultipleMappingRow(i, c.getSystem(), c.getCode(), c.getDisplay()); 923 rowSets.add(row); 924 } 925 926 } 927 } 928 } 929 930 for (ConceptMapGroupComponent grp : map.getGroup()) { 931 for (SourceElementComponent src : grp.getElement()) { 932 MultipleMappingRow row = findExistingRowBySource(rowSets, grp.getSource(), src.getCode(), i); 933 if (row == null) { 934 row = new MultipleMappingRow(i, grp.getSource(), src.getCode(), src.getDisplay()); 935 rowSets.add(row); 936 } 937 if (src.getNoMap()) { 938 row.addTerminus(); 939 } else { 940 List<TargetElementComponent> todo = new ArrayList<>(); 941 for (TargetElementComponent tgt : src.getTarget()) { 942 MultipleMappingRow trow = findExistingRowByTarget(rowSets, grp.getTarget(), tgt.getCode(), i); 943 if (trow == null) { 944 row.addTarget(grp.getTarget(), tgt.getCode(), tgt.getRelationship(), tgt.getComment(), rowSets, i); 945 } else { 946 todo.add(tgt); 947 } 948 } 949 // we've already got a mapping to these targets. So we gather them under the one mapping - but do this after the others are done 950 for (TargetElementComponent t : todo) { 951 MultipleMappingRow trow = findExistingRowByTarget(rowSets, grp.getTarget(), t.getCode(), i); 952 if (row.alreadyHasMappings(i)) { 953 // src is already mapped, and so is target, and now we need to map src to target too 954 // we have to clone src, but we only clone the last 955 trow.cloneSource(i, row.getLastSource(i)); 956 } else { 957 trow.addSource(row, rowSets, t.getRelationship(), t.getComment()); 958 } 959 } 960 } 961 } 962 boolean copy = grp.hasUnmapped() && grp.getUnmapped().getMode() == ConceptMapGroupUnmappedMode.USESOURCECODE; 963 if (copy) { 964 for (MultipleMappingRow row : rowSets) { 965 if (row.rowSets.get(0).cells.size() == i && row.lastSystem().equals(grp.getSource())) { 966 row.addCopy(grp.getTarget()); 967 } 968 } 969 } 970 } 971 for (MultipleMappingRow row : rowSets) { 972 if (row.rowSets.get(0).cells.size() == i) { 973 row.addTerminus(); 974 } 975 } 976 if (map.hasTargetScope()) { 977 List<Coding> codings = advisor.getMembers(rmmContext, map.getTargetScope().primitiveValue()); 978 if (codings != null) { 979 for (Coding c : codings) { 980 MultipleMappingRow row = findExistingRowByTarget(rowSets, c.getSystem(), c.getCode(), i); 981 if (row == null) { 982 row = new MultipleMappingRow(i+1, c.getSystem(), c.getCode(), c.getDisplay()); 983 rowSets.add(row); 984 } else { 985 for (MultipleMappingRowItem cells : row.rowSets) { 986 Cell last = cells.cells.get(cells.cells.size() -1); 987 if (last.system != null && last.system.equals(c.getSystem()) && last.code.equals(c.getCode()) && last.display == null) { 988 last.display = c.getDisplay(); 989 } 990 } 991 } 992 } 993 } 994 } 995 996 } 997 998 private static MultipleMappingRow findExistingRowByTarget(List<MultipleMappingRow> rows, String system, String code, int i) { 999 for (MultipleMappingRow row : rows) { 1000 for (MultipleMappingRowItem cells : row.rowSets) { 1001 if (cells.cells.size() > i + 1 && cells.cells.get(i+1).matches(system, code)) { 1002 return row; 1003 } 1004 } 1005 } 1006 return null; 1007 } 1008 1009 private static MultipleMappingRow findExistingRowBySource(List<MultipleMappingRow> rows, String system, String code, int i) { 1010 for (MultipleMappingRow row : rows) { 1011 for (MultipleMappingRowItem cells : row.rowSets) { 1012 if (cells.cells.size() > i && cells.cells.get(i).matches(system, code)) { 1013 return row; 1014 } 1015 } 1016 } 1017 return null; 1018 } 1019}