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}