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