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