
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.DomainResource; 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); 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() ? display(cm.getDateElement()) : "?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 tr = tbl.tr(); 136 XhtmlNode td = tr.td().colspan(comment ? "4" : "3"); 137 td.tx("Mapping from "); 138 if (grp.hasSource()) { 139 renderCanonical(cm, td, grp.getSource()); 140 } else { 141 td.code("unspecified code system"); 142 } 143 td.tx(" to "); 144 if (grp.hasTarget()) { 145 renderCanonical(cm, td, grp.getTarget()); 146 } else { 147 td.code("unspecified code system"); 148 } 149 for (SourceElementComponent ccl : grp.getElement()) { 150 tr = tbl.tr(); 151 td = tr.td(); 152 td.addText(ccl.getCode()); 153 display = ccl.hasDisplay() ? ccl.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getSource()), versionFromCanonical(grp.getSource()), ccl.getCode()); 154 if (display != null && !isSameCodeAndDisplay(ccl.getCode(), display)) 155 td.tx(" ("+display+")"); 156 TargetElementComponent ccm = ccl.getTarget().get(0); 157 if (!ccm.hasRelationship()) 158 tr.td().tx(":"+"("+ConceptMapRelationship.EQUIVALENT.toCode()+")"); 159 else { 160 if (ccm.getRelationshipElement().hasExtension(ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE)) { 161 String code = ToolingExtensions.readStringExtension(ccm.getRelationshipElement(), ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE); 162 tr.td().ah(eqpath+"#"+code).tx(presentEquivalenceCode(code)); 163 } else { 164 tr.td().ah(eqpath+"#"+ccm.getRelationship().toCode()).tx(presentRelationshipCode(ccm.getRelationship().toCode())); 165 } 166 } 167 td = tr.td(); 168 td.addText(ccm.getCode()); 169 display = ccm.hasDisplay() ? ccm.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getTarget()), versionFromCanonical(grp.getTarget()), ccm.getCode()); 170 if (display != null && !isSameCodeAndDisplay(ccm.getCode(), display)) 171 td.tx(" ("+display+")"); 172 if (comment) 173 tr.td().addText(ccm.getComment()); 174 addUnmapped(tbl, grp); 175 } 176 } else { 177 boolean hasRelationships = false; 178 for (int si = 0; si < grp.getElement().size(); si++) { 179 SourceElementComponent ccl = grp.getElement().get(si); 180 for (int ti = 0; ti < ccl.getTarget().size(); ti++) { 181 TargetElementComponent ccm = ccl.getTarget().get(ti); 182 if (ccm.hasRelationship()) { 183 hasRelationships = true; 184 } 185 } 186 } 187 188 XhtmlNode tbl = x.table( "grid"); 189 XhtmlNode tr = tbl.tr(); 190 XhtmlNode td; 191 tr.td().colspan(Integer.toString(1+sources.size())).b().tx("Source Concept Details"); 192 if (hasRelationships) { 193 tr.td().b().tx("Relationship"); 194 } 195 tr.td().colspan(Integer.toString(1+targets.size())).b().tx("Destination Concept Details"); 196 if (comment) { 197 tr.td().b().tx("Comment"); 198 } 199 tr = tbl.tr(); 200 if (sources.get("code").size() == 1) { 201 String url = sources.get("code").iterator().next(); 202 renderCSDetailsLink(tr, url, true); 203 } else 204 tr.td().b().tx("Code"); 205 for (String s : sources.keySet()) { 206 if (!s.equals("code")) { 207 if (sources.get(s).size() == 1) { 208 String url = sources.get(s).iterator().next(); 209 renderCSDetailsLink(tr, url, false); 210 } else 211 tr.td().b().addText(getDescForConcept(s)); 212 } 213 } 214 if (hasRelationships) { 215 tr.td(); 216 } 217 if (targets.get("code").size() == 1) { 218 String url = targets.get("code").iterator().next(); 219 renderCSDetailsLink(tr, url, true); 220 } else 221 tr.td().b().tx("Code"); 222 for (String s : targets.keySet()) { 223 if (!s.equals("code")) { 224 if (targets.get(s).size() == 1) { 225 String url = targets.get(s).iterator().next(); 226 renderCSDetailsLink(tr, url, false); 227 } else 228 tr.td().b().addText(getDescForConcept(s)); 229 } 230 } 231 if (comment) 232 tr.td(); 233 234 for (int si = 0; si < grp.getElement().size(); si++) { 235 SourceElementComponent ccl = grp.getElement().get(si); 236 boolean slast = si == grp.getElement().size()-1; 237 boolean first = true; 238 if (ccl.hasNoMap() && ccl.getNoMap()) { 239 tr = tbl.tr(); 240 td = tr.td().style("border-right-width: 0px"); 241 if (!first) 242 td.style("border-top-style: none"); 243 else 244 td.style("border-bottom-style: none"); 245 if (sources.get("code").size() == 1) 246 td.addText(ccl.getCode()); 247 else 248 td.addText(grp.getSource()+" / "+ccl.getCode()); 249 display = getDisplayForConcept(systemFromCanonical(grp.getSource()), versionFromCanonical(grp.getSource()), ccl.getCode()); 250 tr.td().style("border-left-width: 0px").tx(display == null ? "" : display); 251 tr.td().colspan("4").style("background-color: #efefef").tx("(not mapped)"); 252 253 } else { 254 for (int ti = 0; ti < ccl.getTarget().size(); ti++) { 255 TargetElementComponent ccm = ccl.getTarget().get(ti); 256 boolean last = ti == ccl.getTarget().size()-1; 257 tr = tbl.tr(); 258 td = tr.td().style("border-right-width: 0px"); 259 if (!first && !last) 260 td.style("border-top-style: none; border-bottom-style: none"); 261 else if (!first) 262 td.style("border-top-style: none"); 263 else if (!last) 264 td.style("border-bottom-style: none"); 265 if (first) { 266 if (sources.get("code").size() == 1) 267 td.addText(ccl.getCode()); 268 else 269 td.addText(grp.getSource()+" / "+ccl.getCode()); 270 display = ccl.hasDisplay() ? ccl.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getSource()), versionFromCanonical(grp.getSource()), ccl.getCode()); 271 td = tr.td(); 272 if (!last) 273 td.style("border-left-width: 0px; border-bottom-style: none"); 274 else 275 td.style("border-left-width: 0px"); 276 td.tx(display == null ? "" : display); 277 } else { 278 td = tr.td(); // for display 279 if (!last) 280 td.style("border-left-width: 0px; border-top-style: none; border-bottom-style: none"); 281 else 282 td.style("border-top-style: none; border-left-width: 0px"); 283 } 284 for (String s : sources.keySet()) { 285 if (!s.equals("code")) { 286 td = tr.td(); 287 if (first) { 288 td.addText(getValue(ccm.getDependsOn(), s, sources.get(s).size() != 1)); 289 display = getDisplay(ccm.getDependsOn(), s); 290 if (display != null) 291 td.tx(" ("+display+")"); 292 } 293 } 294 } 295 first = false; 296 if (hasRelationships) { 297 if (!ccm.hasRelationship()) 298 tr.td(); 299 else { 300 if (ccm.getRelationshipElement().hasExtension(ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE)) { 301 String code = ToolingExtensions.readStringExtension(ccm.getRelationshipElement(), ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE); 302 tr.td().ah(eqpath+"#"+code).tx(presentEquivalenceCode(code)); 303 } else { 304 tr.td().ah(eqpath+"#"+ccm.getRelationship().toCode()).tx(presentRelationshipCode(ccm.getRelationship().toCode())); 305 } 306 } 307 } 308 td = tr.td().style("border-right-width: 0px"); 309 if (targets.get("code").size() == 1) 310 td.addText(ccm.getCode()); 311 else 312 td.addText(grp.getTarget()+" / "+ccm.getCode()); 313 display = ccm.hasDisplay() ? ccm.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getTarget()), versionFromCanonical(grp.getTarget()), ccm.getCode()); 314 tr.td().style("border-left-width: 0px").tx(display == null ? "" : display); 315 316 for (String s : targets.keySet()) { 317 if (!s.equals("code")) { 318 td = tr.td(); 319 td.addText(getValue(ccm.getProduct(), s, targets.get(s).size() != 1)); 320 display = getDisplay(ccm.getProduct(), s); 321 if (display != null) 322 td.tx(" ("+display+")"); 323 } 324 } 325 if (comment) 326 tr.td().addText(ccm.getComment()); 327 } 328 } 329 addUnmapped(tbl, grp); 330 } 331 } 332 } 333 return true; 334 } 335 336 public void describe(XhtmlNode x, ConceptMap cm) { 337 x.tx(display(cm)); 338 } 339 340 public String display(ConceptMap cm) { 341 return cm.present(); 342 } 343 344 private boolean isSameCodeAndDisplay(String code, String display) { 345 String c = code.replace(" ", "").replace("-", "").toLowerCase(); 346 String d = display.replace(" ", "").replace("-", "").toLowerCase(); 347 return c.equals(d); 348 } 349 350 351 private String presentRelationshipCode(String code) { 352 if ("related-to".equals(code)) { 353 return "is related to"; 354 } else if ("equivalent".equals(code)) { 355 return "is equivalent to"; 356 } else if ("source-is-narrower-than-target".equals(code)) { 357 return "is narrower then"; 358 } else if ("source-is-broader-than-target".equals(code)) { 359 return "is broader than"; 360 } else if ("not-related-to".equals(code)) { 361 return "is not related to"; 362 } else { 363 return code; 364 } 365 } 366 367 private String presentEquivalenceCode(String code) { 368 if ("relatedto".equals(code)) { 369 return "is related to"; 370 } else if ("equivalent".equals(code)) { 371 return "is equivalent to"; 372 } else if ("equal".equals(code)) { 373 return "is equal to"; 374 } else if ("wider".equals(code)) { 375 return "maps to wider concept"; 376 } else if ("subsumes".equals(code)) { 377 return "is subsumed by"; 378 } else if ("source-is-broader-than-target".equals(code)) { 379 return "maps to narrower concept"; 380 } else if ("specializes".equals(code)) { 381 return "has specialization"; 382 } else if ("inexact".equals(code)) { 383 return "maps loosely to"; 384 } else if ("unmatched".equals(code)) { 385 return "has no match"; 386 } else if ("disjoint".equals(code)) { 387 return "is not related to"; 388 } else { 389 return code; 390 } 391 } 392 393 public void renderCSDetailsLink(XhtmlNode tr, String url, boolean span2) { 394 CodeSystem cs; 395 XhtmlNode td; 396 cs = getContext().getWorker().fetchCodeSystem(url); 397 td = tr.td(); 398 if (span2) { 399 td.colspan("2"); 400 } 401 td.b().tx("Code"); 402 td.tx(" from "); 403 if (cs == null) 404 td.tx(url); 405 else 406 td.ah(context.fixReference(cs.getUserString("path"))).attribute("title", url).tx(cs.present()); 407 } 408 409 private void addUnmapped(XhtmlNode tbl, ConceptMapGroupComponent grp) { 410 if (grp.hasUnmapped()) { 411// throw new Error("not done yet"); 412 } 413 414 } 415 416 private String getDescForConcept(String s) { 417 if (s.startsWith("http://hl7.org/fhir/v2/element/")) 418 return "v2 "+s.substring("http://hl7.org/fhir/v2/element/".length()); 419 return s; 420 } 421 422 423 424 private String getValue(List<OtherElementComponent> list, String s, boolean withSystem) { 425 for (OtherElementComponent c : list) { 426 if (s.equals(c.getProperty())) 427 if (withSystem) 428 return c.getSystem()+" / "+c.getValue(); 429 else 430 return c.getValue(); 431 } 432 return null; 433 } 434 435 private String getDisplay(List<OtherElementComponent> list, String s) { 436 for (OtherElementComponent c : list) { 437 if (s.equals(c.getProperty())) 438 return getDisplayForConcept(systemFromCanonical(c.getSystem()), versionFromCanonical(c.getSystem()), c.getValue()); 439 } 440 return null; 441 } 442 443}