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}