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