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