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}