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.MappingPropertyComponent;
015import org.hl7.fhir.r5.model.ConceptMap.OtherElementComponent;
016import org.hl7.fhir.r5.model.ConceptMap.SourceElementComponent;
017import org.hl7.fhir.r5.model.ConceptMap.TargetElementComponent;
018import org.hl7.fhir.r5.model.ContactDetail;
019import org.hl7.fhir.r5.model.ContactPoint;
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, false);
040  }
041
042  public boolean render(XhtmlNode x, ConceptMap cm, boolean header) throws FHIRFormatError, DefinitionException, IOException {
043    if (header) {
044      x.h2().addText(cm.getName()+" ("+cm.getUrl()+")");
045    }
046
047    XhtmlNode p = x.para();
048    p.tx("Mapping from ");
049    if (cm.hasSourceScope())
050      AddVsRef(cm.getSourceScope().primitiveValue(), p, cm);
051    else
052      p.tx("(not specified)");
053    p.tx(" to ");
054    if (cm.hasTargetScope())
055      AddVsRef(cm.getTargetScope().primitiveValue(), p, cm);
056    else 
057      p.tx("(not specified)");
058
059    p = x.para();
060    if (cm.getExperimental())
061      p.addText(Utilities.capitalize(cm.getStatus().toString())+" (not intended for production usage). ");
062    else
063      p.addText(Utilities.capitalize(cm.getStatus().toString())+". ");
064    p.tx("Published on "+(cm.hasDate() ? display(cm.getDateElement()) : "?ngen-10?")+" by "+cm.getPublisher());
065    if (!cm.getContact().isEmpty()) {
066      p.tx(" (");
067      boolean firsti = true;
068      for (ContactDetail ci : cm.getContact()) {
069        if (firsti)
070          firsti = false;
071        else
072          p.tx(", ");
073        if (ci.hasName())
074          p.addText(ci.getName()+": ");
075        boolean first = true;
076        for (ContactPoint c : ci.getTelecom()) {
077          if (first)
078            first = false;
079          else
080            p.tx(", ");
081          addTelecom(p, c);
082        }
083      }
084      p.tx(")");
085    }
086    p.tx(". ");
087    p.addText(cm.getCopyright());
088    if (!Utilities.noString(cm.getDescription()))
089      addMarkdown(x, cm.getDescription());
090
091    x.br();
092    int gc = 0;
093    
094    CodeSystem cs = getContext().getWorker().fetchCodeSystem("http://hl7.org/fhir/concept-map-relationship");
095    if (cs == null)
096      cs = getContext().getWorker().fetchCodeSystem("http://hl7.org/fhir/concept-map-equivalence");
097    String eqpath = cs == null ? null : cs.getWebPath();
098
099    for (ConceptMapGroupComponent grp : cm.getGroup()) {
100      String src = grp.getSource();
101      boolean comment = false;
102      boolean ok = true;
103      Map<String, HashSet<String>> props = new HashMap<String, HashSet<String>>();
104      Map<String, HashSet<String>> sources = new HashMap<String, HashSet<String>>();
105      Map<String, HashSet<String>> targets = new HashMap<String, HashSet<String>>();
106      sources.put("code", new HashSet<String>());
107      targets.put("code", new HashSet<String>());
108      sources.get("code").add(grp.getSource());
109      targets.get("code").add(grp.getTarget());
110      for (SourceElementComponent ccl : grp.getElement()) {
111        ok = ok && (ccl.getNoMap() || (ccl.getTarget().size() == 1 && ccl.getTarget().get(0).getDependsOn().isEmpty() && ccl.getTarget().get(0).getProduct().isEmpty()));
112        for (TargetElementComponent ccm : ccl.getTarget()) {
113          comment = comment || !Utilities.noString(ccm.getComment());
114          for (MappingPropertyComponent pp : ccm.getProperty()) {
115            if (!props.containsKey(pp.getCode()))
116              props.put(pp.getCode(), new HashSet<String>());            
117          }
118          for (OtherElementComponent d : ccm.getDependsOn()) {
119            if (!sources.containsKey(d.getAttribute()))
120              sources.put(d.getAttribute(), new HashSet<String>());
121          }
122          for (OtherElementComponent d : ccm.getProduct()) {
123            if (!targets.containsKey(d.getAttribute()))
124              targets.put(d.getAttribute(), new HashSet<String>());
125          }
126        }
127      }
128
129      gc++;
130      if (gc > 1) {
131        x.hr();
132      }
133      XhtmlNode pp = x.para();
134      pp.b().tx("Group "+gc);
135      pp.tx("Mapping from ");
136      if (grp.hasSource()) {
137        renderCanonical(cm, pp, grp.getSource());
138      } else {
139        pp.code("unspecified code system");
140      }
141      pp.tx(" to ");
142      if (grp.hasTarget()) {
143        renderCanonical(cm, pp, grp.getTarget());
144      } else {
145        pp.code("unspecified code system");
146      }
147      
148      String display;
149      if (ok) {
150        // simple
151        XhtmlNode tbl = x.table( "grid");
152        XhtmlNode tr = tbl.tr();
153        tr.td().b().tx("Source Code");
154        tr.td().b().tx("Relationship");
155        tr.td().b().tx("Target Code");
156        if (comment)
157          tr.td().b().tx("Comment");
158        for (SourceElementComponent ccl : grp.getElement()) {
159          tr = tbl.tr();
160          XhtmlNode td = tr.td();
161          td.addText(ccl.getCode());
162          display = ccl.hasDisplay() ? ccl.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getSource()), versionFromCanonical(grp.getSource()), ccl.getCode());
163          if (display != null && !isSameCodeAndDisplay(ccl.getCode(), display))
164            td.tx(" ("+display+")");
165          if (ccl.getNoMap()) {
166            tr.td().colspan(comment ? "3" : "2").style("background-color: #efefef").tx("(not mapped)");
167          } else {
168            TargetElementComponent ccm = ccl.getTarget().get(0);
169            if (!ccm.hasRelationship())
170              tr.td().tx(":"+"("+ConceptMapRelationship.EQUIVALENT.toCode()+")");
171            else {
172              if (ccm.getRelationshipElement().hasExtension(ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE)) {
173                String code = ToolingExtensions.readStringExtension(ccm.getRelationshipElement(), ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE);
174                tr.td().ah(eqpath+"#"+code, code).tx(presentEquivalenceCode(code));                
175              } else {
176                tr.td().ah(eqpath+"#"+ccm.getRelationship().toCode(), ccm.getRelationship().toCode()).tx(presentRelationshipCode(ccm.getRelationship().toCode()));
177              }
178            }
179            td = tr.td();
180            td.addText(ccm.getCode());
181            display = ccm.hasDisplay() ? ccm.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getTarget()), versionFromCanonical(grp.getTarget()), ccm.getCode());
182            if (display != null && !isSameCodeAndDisplay(ccm.getCode(), display))
183              td.tx(" ("+display+")");
184            if (comment)
185              tr.td().addText(ccm.getComment());
186          }
187          addUnmapped(tbl, grp);
188        }
189      } else {
190        boolean hasRelationships = false;
191        for (int si = 0; si < grp.getElement().size(); si++) {
192          SourceElementComponent ccl = grp.getElement().get(si);
193          for (int ti = 0; ti < ccl.getTarget().size(); ti++) {
194            TargetElementComponent ccm = ccl.getTarget().get(ti);
195            if (ccm.hasRelationship()) {
196              hasRelationships = true;
197            }  
198          }
199        }
200        
201        XhtmlNode tbl = x.table( "grid");
202        XhtmlNode tr = tbl.tr();
203        XhtmlNode td;
204        tr.td().colspan(Integer.toString(1+sources.size())).b().tx("Source Concept Details");
205        if (hasRelationships) {
206          tr.td().b().tx("Relationship");
207        }
208        tr.td().colspan(Integer.toString(1+targets.size())).b().tx("Target Concept Details");
209        if (comment) {
210          tr.td().b().tx("Comment");
211        }
212        tr.td().colspan(Integer.toString(1+targets.size())).b().tx("Properties");
213        tr = tbl.tr();
214        if (sources.get("code").size() == 1) {
215          String url = sources.get("code").iterator().next();
216          renderCSDetailsLink(tr, url, true);           
217        } else
218          tr.td().b().tx("Code");
219        for (String s : sources.keySet()) {
220          if (s != null && !s.equals("code")) {
221            if (sources.get(s).size() == 1) {
222              String url = sources.get(s).iterator().next();
223              renderCSDetailsLink(tr, url, false);           
224            } else
225              tr.td().b().addText(getDescForConcept(s));
226          }
227        }
228        if (hasRelationships) {
229          tr.td();
230        }
231        if (targets.get("code").size() == 1) {
232          String url = targets.get("code").iterator().next();
233          renderCSDetailsLink(tr, url, true);           
234        } else
235          tr.td().b().tx("Code");
236        for (String s : targets.keySet()) {
237          if (s != null && !s.equals("code")) {
238            if (targets.get(s).size() == 1) {
239              String url = targets.get(s).iterator().next();
240              renderCSDetailsLink(tr, url, false);           
241            } else
242              tr.td().b().addText(getDescForConcept(s));
243          }
244        }
245        if (comment) {
246          tr.td();
247        }
248        for (String s : props.keySet()) {
249          if (s != null) {
250            if (props.get(s).size() == 1) {
251              String url = props.get(s).iterator().next();
252              renderCSDetailsLink(tr, url, false);           
253            } else
254              tr.td().b().addText(getDescForConcept(s));
255          }
256        }
257
258        for (int si = 0; si < grp.getElement().size(); si++) {
259          SourceElementComponent ccl = grp.getElement().get(si);
260          boolean slast = si == grp.getElement().size()-1;
261          boolean first = true;
262          if (ccl.hasNoMap() && ccl.getNoMap()) {
263            tr = tbl.tr();
264            td = tr.td().style("border-right-width: 0px");
265            if (!first)
266              td.style("border-top-style: none");
267            else 
268              td.style("border-bottom-style: none");
269            if (sources.get("code").size() == 1)
270              td.addText(ccl.getCode());
271            else
272              td.addText(grp.getSource()+" / "+ccl.getCode());
273            display = ccl.hasDisplay() ? ccl.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getSource()), versionFromCanonical(grp.getSource()), ccl.getCode());
274            tr.td().style("border-left-width: 0px").tx(display == null ? "" : display);
275            tr.td().colspan("4").style("background-color: #efefef").tx("(not mapped)");
276
277          } else {
278            for (int ti = 0; ti < ccl.getTarget().size(); ti++) {
279              TargetElementComponent ccm = ccl.getTarget().get(ti);
280              boolean last = ti == ccl.getTarget().size()-1;
281              tr = tbl.tr();
282              td = tr.td().style("border-right-width: 0px");
283              if (!first && !last)
284                td.style("border-top-style: none; border-bottom-style: none");
285              else if (!first)
286                td.style("border-top-style: none");
287              else if (!last)
288                td.style("border-bottom-style: none");
289              if (first) {
290                if (sources.get("code").size() == 1)
291                  td.addText(ccl.getCode());
292                else
293                  td.addText(grp.getSource()+" / "+ccl.getCode());
294                display = ccl.hasDisplay() ? ccl.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getSource()), versionFromCanonical(grp.getSource()), ccl.getCode());
295                td = tr.td();
296                if (!last)
297                  td.style("border-left-width: 0px; border-bottom-style: none");
298                else
299                  td.style("border-left-width: 0px");
300                td.tx(display == null ? "" : display);
301              } else {
302                td = tr.td(); // for display
303                if (!last)
304                  td.style("border-left-width: 0px; border-top-style: none; border-bottom-style: none");
305                else
306                  td.style("border-top-style: none; border-left-width: 0px");
307              }
308              for (String s : sources.keySet()) {
309                if (s != null && !s.equals("code")) {
310                  td = tr.td();
311                  if (first) {
312                    td.addText(getValue(ccm.getDependsOn(), s, sources.get(s).size() != 1));
313                    display = getDisplay(ccm.getDependsOn(), s);
314                    if (display != null)
315                      td.tx(" ("+display+")");
316                  }
317                }
318              }
319              first = false;
320              if (hasRelationships) {
321                if (!ccm.hasRelationship())
322                  tr.td();
323                else {
324                  if (ccm.getRelationshipElement().hasExtension(ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE)) {
325                    String code = ToolingExtensions.readStringExtension(ccm.getRelationshipElement(), ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE);
326                    tr.td().ah(eqpath+"#"+code, code).tx(presentEquivalenceCode(code));                
327                  } else {
328                    tr.td().ah(eqpath+"#"+ccm.getRelationship().toCode(), ccm.getRelationship().toCode()).tx(presentRelationshipCode(ccm.getRelationship().toCode()));
329                  }
330                }
331              }
332              td = tr.td().style("border-right-width: 0px");
333              if (targets.get("code").size() == 1)
334                td.addText(ccm.getCode());
335              else
336                td.addText(grp.getTarget()+" / "+ccm.getCode());
337              display = ccm.hasDisplay() ? ccm.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getTarget()), versionFromCanonical(grp.getTarget()), ccm.getCode());
338              tr.td().style("border-left-width: 0px").tx(display == null ? "" : display);
339
340              for (String s : targets.keySet()) {
341                if (s != null && !s.equals("code")) {
342                  td = tr.td();
343                  td.addText(getValue(ccm.getProduct(), s, targets.get(s).size() != 1));
344                  display = getDisplay(ccm.getProduct(), s);
345                  if (display != null)
346                    td.tx(" ("+display+")");
347                }
348              }
349              if (comment)
350                tr.td().addText(ccm.getComment());
351
352              for (String s : props.keySet()) {
353                if (s != null) {
354                  td = tr.td();
355                  td.addText(getValue(ccm.getProperty(), s));
356                }
357              }
358            }
359          }
360          addUnmapped(tbl, grp);
361        }
362      }
363    }
364    return true;
365  }
366
367  public void describe(XhtmlNode x, ConceptMap cm) {
368    x.tx(display(cm));
369  }
370
371  public String display(ConceptMap cm) {
372    return cm.present();
373  }
374
375  private boolean isSameCodeAndDisplay(String code, String display) {
376    String c = code.replace(" ", "").replace("-", "").toLowerCase();
377    String d = display.replace(" ", "").replace("-", "").toLowerCase();
378    return c.equals(d);
379  }
380
381
382  private String presentRelationshipCode(String code) {
383    if ("related-to".equals(code)) {
384      return "is related to";
385    } else if ("equivalent".equals(code)) {
386      return "is equivalent to";
387    } else if ("source-is-narrower-than-target".equals(code)) {
388      return "is narrower than";
389    } else if ("source-is-broader-than-target".equals(code)) {
390      return "is broader than";
391    } else if ("not-related-to".equals(code)) {
392      return "is not related to";
393    } else {
394      return code;
395    }
396  }
397
398  private String presentEquivalenceCode(String code) {
399    if ("relatedto".equals(code)) {
400      return "is related to";
401    } else if ("equivalent".equals(code)) {
402      return "is equivalent to";
403    } else if ("equal".equals(code)) {
404      return "is equal to";
405    } else if ("wider".equals(code)) {
406      return "maps to wider concept";
407    } else if ("subsumes".equals(code)) {
408      return "is subsumed by";
409    } else if ("source-is-broader-than-target".equals(code)) {
410      return "maps to narrower concept";
411    } else if ("specializes".equals(code)) {
412      return "has specialization";
413    } else if ("inexact".equals(code)) {
414      return "maps loosely to";
415    } else if ("unmatched".equals(code)) {
416      return "has no match";
417    } else if ("disjoint".equals(code)) {
418      return "is not related to";
419    } else {
420      return code;
421    }
422  }
423
424  public void renderCSDetailsLink(XhtmlNode tr, String url, boolean span2) {
425    CodeSystem cs;
426    XhtmlNode td;
427    cs = getContext().getWorker().fetchCodeSystem(url);
428    td = tr.td();
429    if (span2) {
430      td.colspan("2");
431    }
432    td.b().tx("Codes");
433    td.tx(" from ");
434    if (cs == null)
435      td.tx(url);
436    else
437      td.ah(context.fixReference(cs.getWebPath())).attribute("title", url).tx(cs.present());
438  }
439
440  private void addUnmapped(XhtmlNode tbl, ConceptMapGroupComponent grp) {
441    if (grp.hasUnmapped()) {
442//      throw new Error("not done yet");
443    }
444    
445  }
446
447  private String getDescForConcept(String s) {
448    if (s.startsWith("http://hl7.org/fhir/v2/element/"))
449        return "v2 "+s.substring("http://hl7.org/fhir/v2/element/".length());
450    return s;
451  }
452
453
454  private String getValue(List<MappingPropertyComponent> list, String s) {
455    return "todo";
456  }
457
458  private String getValue(List<OtherElementComponent> list, String s, boolean withSystem) {
459    for (OtherElementComponent c : list) {
460      if (s.equals(c.getAttribute()))
461        if (withSystem)
462          return /*c.getSystem()+" / "+*/c.getValue().primitiveValue();
463        else
464          return c.getValue().primitiveValue();
465    }
466    return null;
467  }
468
469  private String getDisplay(List<OtherElementComponent> list, String s) {
470    for (OtherElementComponent c : list) {
471      if (s.equals(c.getAttribute())) {
472        // return getDisplayForConcept(systemFromCanonical(c.getSystem()), versionFromCanonical(c.getSystem()), c.getValue());
473      }
474    }
475    return null;
476  }
477
478}