001package org.hl7.fhir.r5.renderers; 002 003import java.io.IOException; 004import java.util.HashMap; 005import java.util.HashSet; 006import java.util.List; 007import java.util.Map; 008 009import org.hl7.fhir.exceptions.DefinitionException; 010import org.hl7.fhir.exceptions.FHIRFormatError; 011import org.hl7.fhir.r5.model.CodeSystem; 012import org.hl7.fhir.r5.model.ConceptMap; 013import org.hl7.fhir.r5.model.ConceptMap.ConceptMapGroupComponent; 014import org.hl7.fhir.r5.model.ConceptMap.MappingPropertyComponent; 015import org.hl7.fhir.r5.model.ConceptMap.OtherElementComponent; 016import org.hl7.fhir.r5.model.ConceptMap.SourceElementComponent; 017import org.hl7.fhir.r5.model.ConceptMap.TargetElementComponent; 018import org.hl7.fhir.r5.model.ContactDetail; 019import org.hl7.fhir.r5.model.ContactPoint; 020import org.hl7.fhir.r5.model.Enumerations.ConceptMapRelationship; 021import org.hl7.fhir.r5.model.Resource; 022import org.hl7.fhir.r5.renderers.utils.RenderingContext; 023import org.hl7.fhir.r5.renderers.utils.Resolver.ResourceContext; 024import org.hl7.fhir.r5.utils.ToolingExtensions; 025import org.hl7.fhir.utilities.Utilities; 026import org.hl7.fhir.utilities.xhtml.XhtmlNode; 027 028public class ConceptMapRenderer extends TerminologyRenderer { 029 030 public ConceptMapRenderer(RenderingContext context) { 031 super(context); 032 } 033 034 public ConceptMapRenderer(RenderingContext context, ResourceContext rcontext) { 035 super(context, rcontext); 036 } 037 038 public boolean render(XhtmlNode x, Resource dr) throws FHIRFormatError, DefinitionException, IOException { 039 return render(x, (ConceptMap) dr, false); 040 } 041 042 public boolean render(XhtmlNode x, ConceptMap cm, boolean header) throws FHIRFormatError, DefinitionException, IOException { 043 if (header) { 044 x.h2().addText(cm.getName()+" ("+cm.getUrl()+")"); 045 } 046 047 XhtmlNode p = x.para(); 048 p.tx("Mapping from "); 049 if (cm.hasSourceScope()) 050 AddVsRef(cm.getSourceScope().primitiveValue(), p, cm); 051 else 052 p.tx("(not specified)"); 053 p.tx(" to "); 054 if (cm.hasTargetScope()) 055 AddVsRef(cm.getTargetScope().primitiveValue(), p, cm); 056 else 057 p.tx("(not specified)"); 058 059 p = x.para(); 060 if (cm.getExperimental()) 061 p.addText(Utilities.capitalize(cm.getStatus().toString())+" (not intended for production usage). "); 062 else 063 p.addText(Utilities.capitalize(cm.getStatus().toString())+". "); 064 p.tx("Published on "+(cm.hasDate() ? display(cm.getDateElement()) : "?ngen-10?")+" by "+cm.getPublisher()); 065 if (!cm.getContact().isEmpty()) { 066 p.tx(" ("); 067 boolean firsti = true; 068 for (ContactDetail ci : cm.getContact()) { 069 if (firsti) 070 firsti = false; 071 else 072 p.tx(", "); 073 if (ci.hasName()) 074 p.addText(ci.getName()+": "); 075 boolean first = true; 076 for (ContactPoint c : ci.getTelecom()) { 077 if (first) 078 first = false; 079 else 080 p.tx(", "); 081 addTelecom(p, c); 082 } 083 } 084 p.tx(")"); 085 } 086 p.tx(". "); 087 p.addText(cm.getCopyright()); 088 if (!Utilities.noString(cm.getDescription())) 089 addMarkdown(x, cm.getDescription()); 090 091 x.br(); 092 int gc = 0; 093 094 CodeSystem cs = getContext().getWorker().fetchCodeSystem("http://hl7.org/fhir/concept-map-relationship"); 095 if (cs == null) 096 cs = getContext().getWorker().fetchCodeSystem("http://hl7.org/fhir/concept-map-equivalence"); 097 String eqpath = cs == null ? null : cs.getWebPath(); 098 099 for (ConceptMapGroupComponent grp : cm.getGroup()) { 100 String src = grp.getSource(); 101 boolean comment = false; 102 boolean ok = true; 103 Map<String, HashSet<String>> props = new HashMap<String, HashSet<String>>(); 104 Map<String, HashSet<String>> sources = new HashMap<String, HashSet<String>>(); 105 Map<String, HashSet<String>> targets = new HashMap<String, HashSet<String>>(); 106 sources.put("code", new HashSet<String>()); 107 targets.put("code", new HashSet<String>()); 108 sources.get("code").add(grp.getSource()); 109 targets.get("code").add(grp.getTarget()); 110 for (SourceElementComponent ccl : grp.getElement()) { 111 ok = ok && (ccl.getNoMap() || (ccl.getTarget().size() == 1 && ccl.getTarget().get(0).getDependsOn().isEmpty() && ccl.getTarget().get(0).getProduct().isEmpty())); 112 for (TargetElementComponent ccm : ccl.getTarget()) { 113 comment = comment || !Utilities.noString(ccm.getComment()); 114 for (MappingPropertyComponent pp : ccm.getProperty()) { 115 if (!props.containsKey(pp.getCode())) 116 props.put(pp.getCode(), new HashSet<String>()); 117 } 118 for (OtherElementComponent d : ccm.getDependsOn()) { 119 if (!sources.containsKey(d.getAttribute())) 120 sources.put(d.getAttribute(), new HashSet<String>()); 121 } 122 for (OtherElementComponent d : ccm.getProduct()) { 123 if (!targets.containsKey(d.getAttribute())) 124 targets.put(d.getAttribute(), new HashSet<String>()); 125 } 126 } 127 } 128 129 gc++; 130 if (gc > 1) { 131 x.hr(); 132 } 133 XhtmlNode pp = x.para(); 134 pp.b().tx("Group "+gc); 135 pp.tx("Mapping from "); 136 if (grp.hasSource()) { 137 renderCanonical(cm, pp, grp.getSource()); 138 } else { 139 pp.code("unspecified code system"); 140 } 141 pp.tx(" to "); 142 if (grp.hasTarget()) { 143 renderCanonical(cm, pp, grp.getTarget()); 144 } else { 145 pp.code("unspecified code system"); 146 } 147 148 String display; 149 if (ok) { 150 // simple 151 XhtmlNode tbl = x.table( "grid"); 152 XhtmlNode tr = tbl.tr(); 153 tr.td().b().tx("Source Code"); 154 tr.td().b().tx("Relationship"); 155 tr.td().b().tx("Target Code"); 156 if (comment) 157 tr.td().b().tx("Comment"); 158 for (SourceElementComponent ccl : grp.getElement()) { 159 tr = tbl.tr(); 160 XhtmlNode td = tr.td(); 161 td.addText(ccl.getCode()); 162 display = ccl.hasDisplay() ? ccl.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getSource()), versionFromCanonical(grp.getSource()), ccl.getCode()); 163 if (display != null && !isSameCodeAndDisplay(ccl.getCode(), display)) 164 td.tx(" ("+display+")"); 165 if (ccl.getNoMap()) { 166 tr.td().colspan(comment ? "3" : "2").style("background-color: #efefef").tx("(not mapped)"); 167 } else { 168 TargetElementComponent ccm = ccl.getTarget().get(0); 169 if (!ccm.hasRelationship()) 170 tr.td().tx(":"+"("+ConceptMapRelationship.EQUIVALENT.toCode()+")"); 171 else { 172 if (ccm.getRelationshipElement().hasExtension(ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE)) { 173 String code = ToolingExtensions.readStringExtension(ccm.getRelationshipElement(), ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE); 174 tr.td().ah(eqpath+"#"+code, code).tx(presentEquivalenceCode(code)); 175 } else { 176 tr.td().ah(eqpath+"#"+ccm.getRelationship().toCode(), ccm.getRelationship().toCode()).tx(presentRelationshipCode(ccm.getRelationship().toCode())); 177 } 178 } 179 td = tr.td(); 180 td.addText(ccm.getCode()); 181 display = ccm.hasDisplay() ? ccm.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getTarget()), versionFromCanonical(grp.getTarget()), ccm.getCode()); 182 if (display != null && !isSameCodeAndDisplay(ccm.getCode(), display)) 183 td.tx(" ("+display+")"); 184 if (comment) 185 tr.td().addText(ccm.getComment()); 186 } 187 addUnmapped(tbl, grp); 188 } 189 } else { 190 boolean hasRelationships = false; 191 for (int si = 0; si < grp.getElement().size(); si++) { 192 SourceElementComponent ccl = grp.getElement().get(si); 193 for (int ti = 0; ti < ccl.getTarget().size(); ti++) { 194 TargetElementComponent ccm = ccl.getTarget().get(ti); 195 if (ccm.hasRelationship()) { 196 hasRelationships = true; 197 } 198 } 199 } 200 201 XhtmlNode tbl = x.table( "grid"); 202 XhtmlNode tr = tbl.tr(); 203 XhtmlNode td; 204 tr.td().colspan(Integer.toString(1+sources.size())).b().tx("Source Concept Details"); 205 if (hasRelationships) { 206 tr.td().b().tx("Relationship"); 207 } 208 tr.td().colspan(Integer.toString(1+targets.size())).b().tx("Target Concept Details"); 209 if (comment) { 210 tr.td().b().tx("Comment"); 211 } 212 tr.td().colspan(Integer.toString(1+targets.size())).b().tx("Properties"); 213 tr = tbl.tr(); 214 if (sources.get("code").size() == 1) { 215 String url = sources.get("code").iterator().next(); 216 renderCSDetailsLink(tr, url, true); 217 } else 218 tr.td().b().tx("Code"); 219 for (String s : sources.keySet()) { 220 if (s != null && !s.equals("code")) { 221 if (sources.get(s).size() == 1) { 222 String url = sources.get(s).iterator().next(); 223 renderCSDetailsLink(tr, url, false); 224 } else 225 tr.td().b().addText(getDescForConcept(s)); 226 } 227 } 228 if (hasRelationships) { 229 tr.td(); 230 } 231 if (targets.get("code").size() == 1) { 232 String url = targets.get("code").iterator().next(); 233 renderCSDetailsLink(tr, url, true); 234 } else 235 tr.td().b().tx("Code"); 236 for (String s : targets.keySet()) { 237 if (s != null && !s.equals("code")) { 238 if (targets.get(s).size() == 1) { 239 String url = targets.get(s).iterator().next(); 240 renderCSDetailsLink(tr, url, false); 241 } else 242 tr.td().b().addText(getDescForConcept(s)); 243 } 244 } 245 if (comment) { 246 tr.td(); 247 } 248 for (String s : props.keySet()) { 249 if (s != null) { 250 if (props.get(s).size() == 1) { 251 String url = props.get(s).iterator().next(); 252 renderCSDetailsLink(tr, url, false); 253 } else 254 tr.td().b().addText(getDescForConcept(s)); 255 } 256 } 257 258 for (int si = 0; si < grp.getElement().size(); si++) { 259 SourceElementComponent ccl = grp.getElement().get(si); 260 boolean slast = si == grp.getElement().size()-1; 261 boolean first = true; 262 if (ccl.hasNoMap() && ccl.getNoMap()) { 263 tr = tbl.tr(); 264 td = tr.td().style("border-right-width: 0px"); 265 if (!first) 266 td.style("border-top-style: none"); 267 else 268 td.style("border-bottom-style: none"); 269 if (sources.get("code").size() == 1) 270 td.addText(ccl.getCode()); 271 else 272 td.addText(grp.getSource()+" / "+ccl.getCode()); 273 display = ccl.hasDisplay() ? ccl.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getSource()), versionFromCanonical(grp.getSource()), ccl.getCode()); 274 tr.td().style("border-left-width: 0px").tx(display == null ? "" : display); 275 tr.td().colspan("4").style("background-color: #efefef").tx("(not mapped)"); 276 277 } else { 278 for (int ti = 0; ti < ccl.getTarget().size(); ti++) { 279 TargetElementComponent ccm = ccl.getTarget().get(ti); 280 boolean last = ti == ccl.getTarget().size()-1; 281 tr = tbl.tr(); 282 td = tr.td().style("border-right-width: 0px"); 283 if (!first && !last) 284 td.style("border-top-style: none; border-bottom-style: none"); 285 else if (!first) 286 td.style("border-top-style: none"); 287 else if (!last) 288 td.style("border-bottom-style: none"); 289 if (first) { 290 if (sources.get("code").size() == 1) 291 td.addText(ccl.getCode()); 292 else 293 td.addText(grp.getSource()+" / "+ccl.getCode()); 294 display = ccl.hasDisplay() ? ccl.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getSource()), versionFromCanonical(grp.getSource()), ccl.getCode()); 295 td = tr.td(); 296 if (!last) 297 td.style("border-left-width: 0px; border-bottom-style: none"); 298 else 299 td.style("border-left-width: 0px"); 300 td.tx(display == null ? "" : display); 301 } else { 302 td = tr.td(); // for display 303 if (!last) 304 td.style("border-left-width: 0px; border-top-style: none; border-bottom-style: none"); 305 else 306 td.style("border-top-style: none; border-left-width: 0px"); 307 } 308 for (String s : sources.keySet()) { 309 if (s != null && !s.equals("code")) { 310 td = tr.td(); 311 if (first) { 312 td.addText(getValue(ccm.getDependsOn(), s, sources.get(s).size() != 1)); 313 display = getDisplay(ccm.getDependsOn(), s); 314 if (display != null) 315 td.tx(" ("+display+")"); 316 } 317 } 318 } 319 first = false; 320 if (hasRelationships) { 321 if (!ccm.hasRelationship()) 322 tr.td(); 323 else { 324 if (ccm.getRelationshipElement().hasExtension(ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE)) { 325 String code = ToolingExtensions.readStringExtension(ccm.getRelationshipElement(), ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE); 326 tr.td().ah(eqpath+"#"+code, code).tx(presentEquivalenceCode(code)); 327 } else { 328 tr.td().ah(eqpath+"#"+ccm.getRelationship().toCode(), ccm.getRelationship().toCode()).tx(presentRelationshipCode(ccm.getRelationship().toCode())); 329 } 330 } 331 } 332 td = tr.td().style("border-right-width: 0px"); 333 if (targets.get("code").size() == 1) 334 td.addText(ccm.getCode()); 335 else 336 td.addText(grp.getTarget()+" / "+ccm.getCode()); 337 display = ccm.hasDisplay() ? ccm.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getTarget()), versionFromCanonical(grp.getTarget()), ccm.getCode()); 338 tr.td().style("border-left-width: 0px").tx(display == null ? "" : display); 339 340 for (String s : targets.keySet()) { 341 if (s != null && !s.equals("code")) { 342 td = tr.td(); 343 td.addText(getValue(ccm.getProduct(), s, targets.get(s).size() != 1)); 344 display = getDisplay(ccm.getProduct(), s); 345 if (display != null) 346 td.tx(" ("+display+")"); 347 } 348 } 349 if (comment) 350 tr.td().addText(ccm.getComment()); 351 352 for (String s : props.keySet()) { 353 if (s != null) { 354 td = tr.td(); 355 td.addText(getValue(ccm.getProperty(), s)); 356 } 357 } 358 } 359 } 360 addUnmapped(tbl, grp); 361 } 362 } 363 } 364 return true; 365 } 366 367 public void describe(XhtmlNode x, ConceptMap cm) { 368 x.tx(display(cm)); 369 } 370 371 public String display(ConceptMap cm) { 372 return cm.present(); 373 } 374 375 private boolean isSameCodeAndDisplay(String code, String display) { 376 String c = code.replace(" ", "").replace("-", "").toLowerCase(); 377 String d = display.replace(" ", "").replace("-", "").toLowerCase(); 378 return c.equals(d); 379 } 380 381 382 private String presentRelationshipCode(String code) { 383 if ("related-to".equals(code)) { 384 return "is related to"; 385 } else if ("equivalent".equals(code)) { 386 return "is equivalent to"; 387 } else if ("source-is-narrower-than-target".equals(code)) { 388 return "is narrower than"; 389 } else if ("source-is-broader-than-target".equals(code)) { 390 return "is broader than"; 391 } else if ("not-related-to".equals(code)) { 392 return "is not related to"; 393 } else { 394 return code; 395 } 396 } 397 398 private String presentEquivalenceCode(String code) { 399 if ("relatedto".equals(code)) { 400 return "is related to"; 401 } else if ("equivalent".equals(code)) { 402 return "is equivalent to"; 403 } else if ("equal".equals(code)) { 404 return "is equal to"; 405 } else if ("wider".equals(code)) { 406 return "maps to wider concept"; 407 } else if ("subsumes".equals(code)) { 408 return "is subsumed by"; 409 } else if ("source-is-broader-than-target".equals(code)) { 410 return "maps to narrower concept"; 411 } else if ("specializes".equals(code)) { 412 return "has specialization"; 413 } else if ("inexact".equals(code)) { 414 return "maps loosely to"; 415 } else if ("unmatched".equals(code)) { 416 return "has no match"; 417 } else if ("disjoint".equals(code)) { 418 return "is not related to"; 419 } else { 420 return code; 421 } 422 } 423 424 public void renderCSDetailsLink(XhtmlNode tr, String url, boolean span2) { 425 CodeSystem cs; 426 XhtmlNode td; 427 cs = getContext().getWorker().fetchCodeSystem(url); 428 td = tr.td(); 429 if (span2) { 430 td.colspan("2"); 431 } 432 td.b().tx("Codes"); 433 td.tx(" from "); 434 if (cs == null) 435 td.tx(url); 436 else 437 td.ah(context.fixReference(cs.getWebPath())).attribute("title", url).tx(cs.present()); 438 } 439 440 private void addUnmapped(XhtmlNode tbl, ConceptMapGroupComponent grp) { 441 if (grp.hasUnmapped()) { 442// throw new Error("not done yet"); 443 } 444 445 } 446 447 private String getDescForConcept(String s) { 448 if (s.startsWith("http://hl7.org/fhir/v2/element/")) 449 return "v2 "+s.substring("http://hl7.org/fhir/v2/element/".length()); 450 return s; 451 } 452 453 454 private String getValue(List<MappingPropertyComponent> list, String s) { 455 return "todo"; 456 } 457 458 private String getValue(List<OtherElementComponent> list, String s, boolean withSystem) { 459 for (OtherElementComponent c : list) { 460 if (s.equals(c.getAttribute())) 461 if (withSystem) 462 return /*c.getSystem()+" / "+*/c.getValue().primitiveValue(); 463 else 464 return c.getValue().primitiveValue(); 465 } 466 return null; 467 } 468 469 private String getDisplay(List<OtherElementComponent> list, String s) { 470 for (OtherElementComponent c : list) { 471 if (s.equals(c.getAttribute())) { 472 // return getDisplayForConcept(systemFromCanonical(c.getSystem()), versionFromCanonical(c.getSystem()), c.getValue()); 473 } 474 } 475 return null; 476 } 477 478}