001package org.hl7.fhir.r5.renderers;
002
003import java.io.IOException;
004import java.util.ArrayList;
005import java.util.Collections;
006import java.util.Comparator;
007import java.util.HashMap;
008import java.util.HashSet;
009import java.util.List;
010import java.util.Map;
011
012import org.hl7.fhir.exceptions.DefinitionException;
013import org.hl7.fhir.exceptions.FHIRFormatError;
014import org.hl7.fhir.r5.model.CodeSystem;
015import org.hl7.fhir.r5.model.Coding;
016import org.hl7.fhir.r5.model.ConceptMap;
017import org.hl7.fhir.r5.model.ConceptMap.ConceptMapGroupComponent;
018import org.hl7.fhir.r5.model.ConceptMap.ConceptMapGroupUnmappedMode;
019import org.hl7.fhir.r5.model.ConceptMap.MappingPropertyComponent;
020import org.hl7.fhir.r5.model.ConceptMap.OtherElementComponent;
021import org.hl7.fhir.r5.model.ConceptMap.SourceElementComponent;
022import org.hl7.fhir.r5.model.ConceptMap.TargetElementComponent;
023import org.hl7.fhir.r5.model.ContactDetail;
024import org.hl7.fhir.r5.model.ContactPoint;
025import org.hl7.fhir.r5.model.Enumerations.ConceptMapRelationship;
026import org.hl7.fhir.r5.model.Resource;
027import org.hl7.fhir.r5.renderers.ConceptMapRenderer.CollateralDefinition;
028import org.hl7.fhir.r5.renderers.utils.RenderingContext;
029import org.hl7.fhir.r5.renderers.utils.Resolver.ResourceContext;
030import org.hl7.fhir.r5.utils.ToolingExtensions;
031import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
032import org.hl7.fhir.utilities.Utilities;
033import org.hl7.fhir.utilities.xhtml.NodeType;
034import org.hl7.fhir.utilities.xhtml.XhtmlNode;
035
036public class ConceptMapRenderer extends TerminologyRenderer {
037
038  public static class CollateralDefinition {
039    private Resource resource;
040    private String label;
041    public CollateralDefinition(Resource resource, String label) {
042      super();
043      this.resource = resource;
044      this.label = label;
045    }
046    public Resource getResource() {
047      return resource;
048    }
049    public String getLabel() {
050      return label;
051    }
052  }
053
054  public enum RenderMultiRowSortPolicy {
055    UNSORTED, FIRST_COL, LAST_COL
056  }
057
058  public interface IMultiMapRendererAdvisor {
059    public RenderMultiRowSortPolicy sortPolicy(Object rmmContext);
060    public List<Coding> getMembers(Object rmmContext, String uri);
061    public boolean describeMap(Object rmmContext, ConceptMap map, XhtmlNode x);
062    public boolean hasCollateral(Object rmmContext);
063    public List<CollateralDefinition> getCollateral(Object rmmContext, String uri); // URI identifies which column the collateral is for
064    public String getLink(Object rmmContext, String system, String code);
065    public boolean makeMapLinks();
066  }
067  
068  public static class MultipleMappingRowSorter implements Comparator<MultipleMappingRow> {
069
070    private boolean first;
071    
072    protected MultipleMappingRowSorter(boolean first) {
073      super();
074      this.first = first;
075    }
076
077    @Override
078    public int compare(MultipleMappingRow o1, MultipleMappingRow o2) {
079      String s1 = first ? o1.firstCode() : o1.lastCode();
080      String s2 = first ? o2.firstCode() : o2.lastCode();
081      return s1.compareTo(s2);
082    }
083  }
084
085  public static class Cell {
086
087    private String system;
088    private String code;
089    private String display;
090    private String relationship;
091    private String relComment;
092    public boolean renderedRel;
093    public boolean renderedCode;
094    private Cell clone;
095    
096    protected Cell() {
097      super();
098    }
099
100    public Cell(String system, String code, String display) {
101      this.system = system;
102      this.code = code;
103      this.display = display;
104    }
105
106    public Cell(String system, String code, String relationship, String comment) {
107      this.system = system;
108      this.code = code;
109      this.relationship = relationship;
110      this.relComment = comment;
111    }
112
113    public boolean matches(String system, String code) {
114      return (system != null && system.equals(this.system)) && (code != null && code.equals(this.code));
115    }
116
117    public String present() {
118      if (system == null) {
119        return code;
120      } else {
121        return code; //+(clone == null ? "" : " (@"+clone.code+")");
122      }
123    }
124
125    public Cell copy(boolean clone) {
126      Cell res = new Cell();
127      res.system = system;
128      res.code = code;
129      res.display = display;
130      res.relationship = relationship;
131      res.relComment = relComment;
132      res.renderedRel = renderedRel;
133      res.renderedCode = renderedCode;
134      if (clone) {
135        res.clone = this;
136      }
137      return res;
138    }
139
140    @Override
141    public String toString() {
142      return relationship+" "+system + "#" + code + " \"" + display + "\"";
143    }
144    
145  }
146  
147
148  public static class MultipleMappingRowItem {
149    List<Cell> cells = new ArrayList<>();
150
151    @Override
152    public String toString() {
153      CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
154      for (Cell cell : cells) {
155        if (cell.relationship != null) {
156          b.append(cell.relationship+cell.code);
157        } else {
158          b.append(cell.code);
159        }
160      }
161      return b.toString();
162    }
163  }
164  
165  public static class MultipleMappingRow {
166    private List<MultipleMappingRowItem> rowSets = new ArrayList<>();
167    private MultipleMappingRow stickySource;
168
169    public MultipleMappingRow(int i, String system, String code, String display) {
170      MultipleMappingRowItem row = new MultipleMappingRowItem();
171      rowSets.add(row);
172      for (int c = 0; c < i; c++) {
173        row.cells.add(new Cell()); // blank cell spaces
174      }
175      row.cells.add(new Cell(system, code, display));
176    }
177
178
179    public MultipleMappingRow(MultipleMappingRow stickySource) {
180      this.stickySource = stickySource;
181    }
182
183    @Override
184    public String toString() {
185      CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
186      for (MultipleMappingRowItem rowSet : rowSets) {
187        b.append(""+rowSet.cells.size());
188      }
189      CommaSeparatedStringBuilder b2 = new CommaSeparatedStringBuilder(";");
190      for (MultipleMappingRowItem rowSet : rowSets) {
191        b2.append(rowSet.toString());
192      }
193      return ""+rowSets.size()+" ["+b.toString()+"] ("+b2.toString()+")";
194    }
195
196
197    public String lastCode() {
198      MultipleMappingRowItem first = rowSets.get(0);
199      for (int i = first.cells.size()-1; i >= 0; i--) {
200        if (first.cells.get(i).code != null) {
201          return first.cells.get(i).code;
202        }
203      }
204      return "";
205    }
206
207    public String firstCode() {
208      MultipleMappingRowItem first = rowSets.get(0);
209      for (int i = 0; i < first.cells.size(); i++) {
210        if (first.cells.get(i).code != null) {
211          return first.cells.get(i).code;
212        }
213      }
214      return "";
215    }
216
217    public void addSource(MultipleMappingRow sourceRow, List<MultipleMappingRow> rowList, ConceptMapRelationship relationship, String comment) {
218      // we already have a row, and we're going to collapse the rows on sourceRow into here, and add a matching terminus 
219      assert sourceRow.rowSets.get(0).cells.size() == rowSets.get(0).cells.size()-1;
220      rowList.remove(sourceRow);
221      Cell template = rowSets.get(0).cells.get(rowSets.get(0).cells.size()-1);
222      for (MultipleMappingRowItem row : sourceRow.rowSets) {
223        row.cells.add(new Cell(template.system, template.code, relationship.getSymbol(), comment));
224      }
225      rowSets.addAll(sourceRow.rowSets);
226    }
227
228    public void addTerminus() {
229      for (MultipleMappingRowItem row : rowSets) {
230        row.cells.add(new Cell(null, null, "X", null));
231      }
232    }
233
234    public void addTarget(String system, String code, ConceptMapRelationship relationship, String comment, List<MultipleMappingRow> sets, int colCount) {
235      if (rowSets.get(0).cells.size() == colCount+1) { // if it's already has a target for this col then we have to clone (and split) the rows
236        for (MultipleMappingRowItem row : rowSets) {
237          row.cells.add(new Cell(system, code, relationship.getSymbol(), comment));
238        }
239      } else {
240        MultipleMappingRow nrow = new MultipleMappingRow(this);
241        for (MultipleMappingRowItem row : rowSets) {
242          MultipleMappingRowItem n = new MultipleMappingRowItem();
243          for (int i = 0; i < row.cells.size()-1; i++) { // note to skip the last
244            n.cells.add(row.cells.get(i).copy(true));
245          }
246          n.cells.add(new Cell(system, code, relationship.getSymbol(), comment));
247          nrow.rowSets.add(n);
248        }
249        sets.add(sets.indexOf(this), nrow);
250      }
251    }
252
253    public String lastSystem() {
254      MultipleMappingRowItem first = rowSets.get(0);
255      for (int i = first.cells.size()-1; i >= 0; i--) {
256        if (first.cells.get(i).system != null) {
257          return first.cells.get(i).system;
258        }
259      }
260      return "";
261    }
262
263    public void addCopy(String system) {
264      for (MultipleMappingRowItem row : rowSets) {
265        row.cells.add(new Cell(system, lastCode(), "=", null));
266      }
267    }
268
269
270    public boolean alreadyHasMappings(int i) {
271      for (MultipleMappingRowItem row : rowSets) {
272        if (row.cells.size() > i+1) {
273          return true;
274        }
275      }
276      return false;
277    }
278
279
280    public Cell getLastSource(int i) {
281      for (MultipleMappingRowItem row : rowSets) {
282        return row.cells.get(i+1);
283      }
284      throw new Error("Should not get here");   // return null
285    }
286
287
288    public void cloneSource(int i, Cell cell) {
289      MultipleMappingRowItem row = new MultipleMappingRowItem();
290      rowSets.add(row);
291      for (int c = 0; c < i-1; c++) {
292        row.cells.add(new Cell()); // blank cell spaces
293      }
294      row.cells.add(cell.copy(true));
295      row.cells.add(rowSets.get(0).cells.get(rowSets.get(0).cells.size()-1).copy(false));      
296    }
297  }
298
299  public ConceptMapRenderer(RenderingContext context) {
300    super(context);
301  }
302
303  public ConceptMapRenderer(RenderingContext context, ResourceContext rcontext) {
304    super(context, rcontext);
305  }
306  
307  public boolean render(XhtmlNode x, Resource dr) throws FHIRFormatError, DefinitionException, IOException {
308    return render(x, (ConceptMap) dr, false);
309  }
310
311  public boolean render(XhtmlNode x, ConceptMap cm, boolean header) throws FHIRFormatError, DefinitionException, IOException {
312    if (header) {
313      x.h2().addText(cm.getName()+" ("+cm.getUrl()+")");
314    }
315
316    XhtmlNode p = x.para();
317    p.tx(context.formatPhrase(RenderingContext.CONC_MAP_FROM) + " ");
318    if (cm.hasSourceScope())
319      AddVsRef(cm.getSourceScope().primitiveValue(), p, cm);
320    else
321      p.tx(context.formatPhrase(RenderingContext.CONC_MAP_NOT_SPEC));
322    p.tx(" "+ (context.formatPhrase(RenderingContext.CONC_MAP_TO) + " "));
323    if (cm.hasTargetScope())
324      AddVsRef(cm.getTargetScope().primitiveValue(), p, cm);
325    else 
326      p.tx(context.formatPhrase(RenderingContext.CONC_MAP_NOT_SPEC));
327
328    p = x.para();
329    if (cm.getExperimental())
330      p.addText(Utilities.capitalize(cm.getStatus().toString())+" "+ (context.formatPhrase(RenderingContext.CONC_MAP_NO_PROD_USE) + " "));
331    else
332      p.addText(Utilities.capitalize(cm.getStatus().toString())+". ");
333    p.tx(context.formatPhrase(RenderingContext.CONC_MAP_PUB_ON, (cm.hasDate() ? display(cm.getDateElement()) : "?ngen-10?")+" by "+cm.getPublisher()) + " ");
334    if (!cm.getContact().isEmpty()) {
335      p.tx(" (");
336      boolean firsti = true;
337      for (ContactDetail ci : cm.getContact()) {
338        if (firsti)
339          firsti = false;
340        else
341          p.tx(", ");
342        if (ci.hasName())
343          p.addText(ci.getName()+": ");
344        boolean first = true;
345        for (ContactPoint c : ci.getTelecom()) {
346          if (first)
347            first = false;
348          else
349            p.tx(", ");
350          addTelecom(p, c);
351        }
352      }
353      p.tx(")");
354    }
355    p.tx(". ");
356    p.addText(cm.getCopyright());
357    if (!Utilities.noString(cm.getDescription()))
358      addMarkdown(x, cm.getDescription());
359
360    x.br();
361    int gc = 0;
362    
363    CodeSystem cs = getContext().getWorker().fetchCodeSystem("http://hl7.org/fhir/concept-map-relationship");
364    if (cs == null)
365      cs = getContext().getWorker().fetchCodeSystem("http://hl7.org/fhir/concept-map-equivalence");
366    String eqpath = cs == null ? null : cs.getWebPath();
367
368    for (ConceptMapGroupComponent grp : cm.getGroup()) {
369      String src = grp.getSource();
370      boolean comment = false;
371      boolean ok = true;
372      Map<String, HashSet<String>> props = new HashMap<String, HashSet<String>>();
373      Map<String, HashSet<String>> sources = new HashMap<String, HashSet<String>>();
374      Map<String, HashSet<String>> targets = new HashMap<String, HashSet<String>>();
375      sources.put("code", new HashSet<String>());
376      targets.put("code", new HashSet<String>());
377      sources.get("code").add(grp.getSource());
378      targets.get("code").add(grp.getTarget());
379      for (SourceElementComponent ccl : grp.getElement()) {
380        ok = ok && (ccl.getNoMap() || (ccl.getTarget().size() == 1 && ccl.getTarget().get(0).getDependsOn().isEmpty() && ccl.getTarget().get(0).getProduct().isEmpty()));
381        for (TargetElementComponent ccm : ccl.getTarget()) {
382          comment = comment || !Utilities.noString(ccm.getComment());
383          for (MappingPropertyComponent pp : ccm.getProperty()) {
384            if (!props.containsKey(pp.getCode()))
385              props.put(pp.getCode(), new HashSet<String>());            
386          }
387          for (OtherElementComponent d : ccm.getDependsOn()) {
388            if (!sources.containsKey(d.getAttribute()))
389              sources.put(d.getAttribute(), new HashSet<String>());
390          }
391          for (OtherElementComponent d : ccm.getProduct()) {
392            if (!targets.containsKey(d.getAttribute()))
393              targets.put(d.getAttribute(), new HashSet<String>());
394          }
395        }
396      }
397
398      gc++;
399      if (gc > 1) {
400        x.hr();
401      }
402      XhtmlNode pp = x.para();
403      pp.b().tx(context.formatPhrase(RenderingContext.CONC_MAP_GRP, gc) + " ");
404      pp.tx(context.formatPhrase(RenderingContext.CONC_MAP_FROM) + " ");
405      if (grp.hasSource()) {
406        renderCanonical(cm, pp, grp.getSource());
407      } else {
408        pp.code(context.formatPhrase(RenderingContext.CONC_MAP_CODE_SYS_UNSPEC));
409      }
410      pp.tx(" to ");
411      if (grp.hasTarget()) {
412        renderCanonical(cm, pp, grp.getTarget());
413      } else {
414        pp.code(context.formatPhrase(RenderingContext.CONC_MAP_CODE_SYS_UNSPEC));
415      }
416      
417      String display;
418      if (ok) {
419        // simple
420        XhtmlNode tbl = x.table( "grid");
421        XhtmlNode tr = tbl.tr();
422        tr.td().b().tx(context.formatPhrase(RenderingContext.CONC_MAP_SOURCE));
423        tr.td().b().tx(context.formatPhrase(RenderingContext.CONC_MAP_REL));
424        tr.td().b().tx(context.formatPhrase(RenderingContext.CONC_MAP_TRGT));
425        if (comment)
426          tr.td().b().tx(context.formatPhrase(RenderingContext.GENERAL_COMMENT));
427        for (SourceElementComponent ccl : grp.getElement()) {
428          tr = tbl.tr();
429          XhtmlNode td = tr.td();
430          td.addText(ccl.getCode());
431          display = ccl.hasDisplay() ? ccl.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getSource()), versionFromCanonical(grp.getSource()), ccl.getCode());
432          if (display != null && !isSameCodeAndDisplay(ccl.getCode(), display))
433            td.tx(" ("+display+")");
434          if (ccl.getNoMap()) {
435            tr.td().colspan(comment ? "3" : "2").style("background-color: #efefef").tx("(not mapped)");
436          } else {
437            TargetElementComponent ccm = ccl.getTarget().get(0);
438            if (!ccm.hasRelationship())
439              tr.td().tx(":"+"("+ConceptMapRelationship.EQUIVALENT.toCode()+")");
440            else {
441              if (ccm.hasExtension(ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE)) {
442                String code = ToolingExtensions.readStringExtension(ccm, ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE);
443                tr.td().ah(eqpath+"#"+code, code).tx(presentEquivalenceCode(code));                
444              } else {
445                tr.td().ah(eqpath+"#"+ccm.getRelationship().toCode(), ccm.getRelationship().toCode()).tx(presentRelationshipCode(ccm.getRelationship().toCode()));
446              }
447            }
448            td = tr.td();
449            td.addText(ccm.getCode());
450            display = ccm.hasDisplay() ? ccm.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getTarget()), versionFromCanonical(grp.getTarget()), ccm.getCode());
451            if (display != null && !isSameCodeAndDisplay(ccm.getCode(), display))
452              td.tx(" ("+display+")");
453            if (comment)
454              tr.td().addText(ccm.getComment());
455          }
456          addUnmapped(tbl, grp);
457        }      
458      } else {
459        boolean hasRelationships = false;
460        for (int si = 0; si < grp.getElement().size(); si++) {
461          SourceElementComponent ccl = grp.getElement().get(si);
462          for (int ti = 0; ti < ccl.getTarget().size(); ti++) {
463            TargetElementComponent ccm = ccl.getTarget().get(ti);
464            if (ccm.hasRelationship()) {
465              hasRelationships = true;
466            }  
467          }
468        }
469        
470        XhtmlNode tbl = x.table( "grid");
471        XhtmlNode tr = tbl.tr();
472        XhtmlNode td;
473        tr.td().colspan(Integer.toString(1+sources.size())).b().tx(context.formatPhrase(RenderingContext.CONC_MAP_SRC_DET));
474        if (hasRelationships) {
475          tr.td().b().tx(context.formatPhrase(RenderingContext.CONC_MAP_REL));
476        }
477        tr.td().colspan(Integer.toString(1+targets.size())).b().tx(context.formatPhrase(RenderingContext.CONC_MAP_TRGT_DET));
478        if (comment) {
479          tr.td().b().tx(context.formatPhrase(RenderingContext.GENERAL_COMMENT));
480        }
481        tr.td().colspan(Integer.toString(1+targets.size())).b().tx(context.formatPhrase(RenderingContext.GENERAL_PROPS));
482        tr = tbl.tr();
483        if (sources.get("code").size() == 1) {
484          String url = sources.get("code").iterator().next();
485          renderCSDetailsLink(tr, url, true);           
486        } else
487          tr.td().b().tx(context.formatPhrase(RenderingContext.GENERAL_CODE));
488        for (String s : sources.keySet()) {
489          if (s != null && !s.equals("code")) {
490            if (sources.get(s).size() == 1) {
491              String url = sources.get(s).iterator().next();
492              renderCSDetailsLink(tr, url, false);           
493            } else
494              tr.td().b().addText(getDescForConcept(s));
495          }
496        }
497        if (hasRelationships) {
498          tr.td();
499        }
500        if (targets.get("code").size() == 1) {
501          String url = targets.get("code").iterator().next();
502          renderCSDetailsLink(tr, url, true);           
503        } else
504          tr.td().b().tx(context.formatPhrase(RenderingContext.GENERAL_CODE));
505        for (String s : targets.keySet()) {
506          if (s != null && !s.equals("code")) {
507            if (targets.get(s).size() == 1) {
508              String url = targets.get(s).iterator().next();
509              renderCSDetailsLink(tr, url, false);           
510            } else
511              tr.td().b().addText(getDescForConcept(s));
512          }
513        }
514        if (comment) {
515          tr.td();
516        }
517        for (String s : props.keySet()) {
518          if (s != null) {
519            if (props.get(s).size() == 1) {
520              String url = props.get(s).iterator().next();
521              renderCSDetailsLink(tr, url, false);           
522            } else
523              tr.td().b().addText(getDescForConcept(s));
524          }
525        }
526
527        for (int si = 0; si < grp.getElement().size(); si++) {
528          SourceElementComponent ccl = grp.getElement().get(si);
529          boolean slast = si == grp.getElement().size()-1;
530          boolean first = true;
531          if (ccl.hasNoMap() && ccl.getNoMap()) {
532            tr = tbl.tr();
533            td = tr.td().style("border-right-width: 0px");
534            if (!first)
535              td.style("border-top-style: none");
536            else 
537              td.style("border-bottom-style: none");
538            if (sources.get("code").size() == 1)
539              td.addText(ccl.getCode());
540            else
541              td.addText(grp.getSource()+" / "+ccl.getCode());
542            display = ccl.hasDisplay() ? ccl.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getSource()), versionFromCanonical(grp.getSource()), ccl.getCode());
543            tr.td().style("border-left-width: 0px").tx(display == null ? "" : display);
544            tr.td().colspan("4").style("background-color: #efefef").tx("(not mapped)");
545
546          } else {
547            for (int ti = 0; ti < ccl.getTarget().size(); ti++) {
548              TargetElementComponent ccm = ccl.getTarget().get(ti);
549              boolean last = ti == ccl.getTarget().size()-1;
550              tr = tbl.tr();
551              td = tr.td().style("border-right-width: 0px");
552              if (!first && !last)
553                td.style("border-top-style: none; border-bottom-style: none");
554              else if (!first)
555                td.style("border-top-style: none");
556              else if (!last)
557                td.style("border-bottom-style: none");
558              if (first) {
559                if (sources.get("code").size() == 1)
560                  td.addText(ccl.getCode());
561                else
562                  td.addText(grp.getSource()+" / "+ccl.getCode());
563                display = ccl.hasDisplay() ? ccl.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getSource()), versionFromCanonical(grp.getSource()), ccl.getCode());
564                td = tr.td();
565                if (!last)
566                  td.style("border-left-width: 0px; border-bottom-style: none");
567                else
568                  td.style("border-left-width: 0px");
569                td.tx(display == null ? "" : display);
570              } else {
571                td = tr.td(); // for display
572                if (!last)
573                  td.style("border-left-width: 0px; border-top-style: none; border-bottom-style: none");
574                else
575                  td.style("border-top-style: none; border-left-width: 0px");
576              }
577              for (String s : sources.keySet()) {
578                if (s != null && !s.equals("code")) {
579                  td = tr.td();
580                  if (first) {
581                    td.addText(getValue(ccm.getDependsOn(), s, sources.get(s).size() != 1));
582                    display = getDisplay(ccm.getDependsOn(), s);
583                    if (display != null)
584                      td.tx(" ("+display+")");
585                  }
586                }
587              }
588              first = false;
589              if (hasRelationships) {
590                if (!ccm.hasRelationship())
591                  tr.td();
592                else {
593                  if (ccm.getRelationshipElement().hasExtension(ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE)) {
594                    String code = ToolingExtensions.readStringExtension(ccm.getRelationshipElement(), ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE);
595                    tr.td().ah(eqpath+"#"+code, code).tx(presentEquivalenceCode(code));                
596                  } else {
597                    tr.td().ah(eqpath+"#"+ccm.getRelationship().toCode(), ccm.getRelationship().toCode()).tx(presentRelationshipCode(ccm.getRelationship().toCode()));
598                  }
599                }
600              }
601              td = tr.td().style("border-right-width: 0px");
602              if (targets.get("code").size() == 1)
603                td.addText(ccm.getCode());
604              else
605                td.addText(grp.getTarget()+" / "+ccm.getCode());
606              display = ccm.hasDisplay() ? ccm.getDisplay() : getDisplayForConcept(systemFromCanonical(grp.getTarget()), versionFromCanonical(grp.getTarget()), ccm.getCode());
607              tr.td().style("border-left-width: 0px").tx(display == null ? "" : display);
608
609              for (String s : targets.keySet()) {
610                if (s != null && !s.equals("code")) {
611                  td = tr.td();
612                  td.addText(getValue(ccm.getProduct(), s, targets.get(s).size() != 1));
613                  display = getDisplay(ccm.getProduct(), s);
614                  if (display != null)
615                    td.tx(" ("+display+")");
616                }
617              }
618              if (comment)
619                tr.td().addText(ccm.getComment());
620
621              for (String s : props.keySet()) {
622                if (s != null) {
623                  td = tr.td();
624                  td.addText(getValue(ccm.getProperty(), s));
625                }
626              }
627            }
628          }
629          addUnmapped(tbl, grp);
630        }
631      }
632    }
633    return true;
634  }
635
636  public void describe(XhtmlNode x, ConceptMap cm) {
637    x.tx(display(cm));
638  }
639
640  public String display(ConceptMap cm) {
641    return cm.present();
642  }
643
644  private boolean isSameCodeAndDisplay(String code, String display) {
645    String c = code.replace(" ", "").replace("-", "").toLowerCase();
646    String d = display.replace(" ", "").replace("-", "").toLowerCase();
647    return c.equals(d);
648  }
649
650
651  private String presentRelationshipCode(String code) {
652    if ("related-to".equals(code)) {
653      return "is related to";
654    } else if ("equivalent".equals(code)) {
655      return "is equivalent to";
656    } else if ("source-is-narrower-than-target".equals(code)) {
657      return "is narrower than";
658    } else if ("source-is-broader-than-target".equals(code)) {
659      return "is broader than";
660    } else if ("not-related-to".equals(code)) {
661      return "is not related to";
662    } else {
663      return code;
664    }
665  }
666
667  private String presentEquivalenceCode(String code) {
668    if ("relatedto".equals(code)) {
669      return "is related to";
670    } else if ("equivalent".equals(code)) {
671      return "is equivalent to";
672    } else if ("equal".equals(code)) {
673      return "is equal to";
674    } else if ("wider".equals(code)) {
675      return "maps to wider concept";
676    } else if ("subsumes".equals(code)) {
677      return "is subsumed by";
678    } else if ("source-is-broader-than-target".equals(code)) {
679      return "maps to narrower concept";
680    } else if ("specializes".equals(code)) {
681      return "has specialization";
682    } else if ("inexact".equals(code)) {
683      return "maps loosely to";
684    } else if ("unmatched".equals(code)) {
685      return "has no match";
686    } else if ("disjoint".equals(code)) {
687      return "is not related to";
688    } else {
689      return code;
690    }
691  }
692
693  public void renderCSDetailsLink(XhtmlNode tr, String url, boolean span2) {
694    CodeSystem cs;
695    XhtmlNode td;
696    cs = getContext().getWorker().fetchCodeSystem(url);
697    td = tr.td();
698    if (span2) {
699      td.colspan("2");
700    }
701    td.b().tx(context.formatPhrase(RenderingContext.CONC_MAP_CODES));
702    td.tx(" " + (context.formatPhrase(RenderingContext.CONC_MAP_FRM) + " "));
703    if (cs == null)
704      td.tx(url);
705    else
706      td.ah(context.fixReference(cs.getWebPath())).attribute("title", url).tx(cs.present());
707  }
708
709  private void addUnmapped(XhtmlNode tbl, ConceptMapGroupComponent grp) {
710    if (grp.hasUnmapped()) {
711//      throw new Error("not done yet");
712    }
713    
714  }
715
716  private String getDescForConcept(String s) {
717    if (s.startsWith("http://hl7.org/fhir/v2/element/"))
718        return "v2 "+s.substring("http://hl7.org/fhir/v2/element/".length());
719    return s;
720  }
721
722
723  private String getValue(List<MappingPropertyComponent> list, String s) {
724    return "todo";
725  }
726
727  private String getValue(List<OtherElementComponent> list, String s, boolean withSystem) {
728    for (OtherElementComponent c : list) {
729      if (s.equals(c.getAttribute()))
730        if (withSystem)
731          return /*c.getSystem()+" / "+*/c.getValue().primitiveValue();
732        else
733          return c.getValue().primitiveValue();
734    }
735    return null;
736  }
737
738  private String getDisplay(List<OtherElementComponent> list, String s) {
739    for (OtherElementComponent c : list) {
740      if (s.equals(c.getAttribute())) {
741        // return getDisplayForConcept(systemFromCanonical(c.getSystem()), versionFromCanonical(c.getSystem()), c.getValue());
742      }
743    }
744    return null;
745  }
746
747  public static XhtmlNode renderMultipleMaps(String start, List<ConceptMap> maps, IMultiMapRendererAdvisor advisor, Object rmmContext) {
748    // 1+1 column for each provided map
749    List<MultipleMappingRow> rowSets = new ArrayList<>();
750    for (int i = 0; i < maps.size(); i++) {
751      populateRows(rowSets, maps.get(i), i, advisor, rmmContext);
752    }
753    collateRows(rowSets);
754    if (advisor.sortPolicy(rmmContext) != RenderMultiRowSortPolicy.UNSORTED) {
755      Collections.sort(rowSets, new MultipleMappingRowSorter(advisor.sortPolicy(rmmContext) == RenderMultiRowSortPolicy.FIRST_COL));
756    }
757    XhtmlNode div = new XhtmlNode(NodeType.Element, "div");
758    XhtmlNode tbl = div.table("none").style("text-align: left; border-spacing: 0; padding: 5px");
759    XhtmlNode tr = tbl.tr();
760    styleCell(tr.td(), false, true, 5).b().tx(start);
761    for (ConceptMap map : maps) {
762      XhtmlNode td = styleCell(tr.td(), false, true, 5).colspan(2);
763      if (!advisor.describeMap(rmmContext, map, td)) {
764        if (map.hasWebPath() && advisor.makeMapLinks()) {
765          td.b().ah(map.getWebPath(), map.getVersionedUrl()).tx(map.present());
766        } else {
767          td.b().tx(map.present());
768        }
769      }
770    }
771    if (advisor.hasCollateral(rmmContext)) {
772      tr = tbl.tr();
773      renderLinks(styleCell(tr.td(), false, true, 5), advisor.getCollateral(rmmContext, null));
774      for (ConceptMap map : maps) {
775        renderLinks(styleCell(tr.td(), false, true, 5).colspan(2), advisor.getCollateral(rmmContext, map.getUrl()));      
776      }
777    }
778    for (MultipleMappingRow row : rowSets) {
779      renderMultiRow(tbl, row, maps, advisor, rmmContext);
780    }
781    return div;
782  }
783
784  private static void renderLinks(XhtmlNode td, List<CollateralDefinition> collateral) {
785    if (collateral.size() > 0) {
786      td.tx( "Links:");
787      td.tx(" ");
788      boolean first = true;
789      for (CollateralDefinition c : collateral) {
790        if (first) first = false; else td.tx(", ");
791        td.ah(c.getResource().getWebPath()).tx(c.getLabel());
792      }
793    }
794  }
795
796  private static void collateRows(List<MultipleMappingRow> rowSets) {
797    List<MultipleMappingRow> toDelete = new ArrayList<ConceptMapRenderer.MultipleMappingRow>();
798    for (MultipleMappingRow rowSet : rowSets) {
799      MultipleMappingRow tgt = rowSet.stickySource;
800      while (toDelete.contains(tgt)) {
801        tgt = tgt.stickySource;
802      }
803      if (tgt != null && rowSets.contains(tgt)) {
804        tgt.rowSets.addAll(rowSet.rowSets);
805        toDelete.add(rowSet);
806      }
807    }
808    rowSets.removeAll(toDelete);    
809  }
810
811  private static void renderMultiRow(XhtmlNode tbl, MultipleMappingRow rows, List<ConceptMap> maps, IMultiMapRendererAdvisor advisor, Object rmmContext) {
812    int rowCounter = 0;
813    for (MultipleMappingRowItem row : rows.rowSets) {
814      XhtmlNode tr = tbl.tr();
815      boolean first = true;
816      int cellCounter = 0;
817      Cell last = null;
818      for (Cell cell : row.cells) {
819        if (first) {     
820          if (!cell.renderedCode) { 
821            int c = 1;
822            for (int i = rowCounter + 1; i < rows.rowSets.size(); i++) {
823              if (cell.code != null && rows.rowSets.get(i).cells.size() > cellCounter && cell.code.equals(rows.rowSets.get(i).cells.get(cellCounter).code)) {
824                rows.rowSets.get(i).cells.get(cellCounter).renderedCode = true;
825                c++;
826              } else {
827                break;
828              }
829            }  
830            if (cell.code == null) {
831              styleCell(tr.td(), rowCounter == 0, true, 5).rowspan(c).style("background-color: #eeeeee");
832            } else {
833              String link = advisor.getLink(rmmContext, cell.system, cell.code);
834              XhtmlNode x = null;
835              if (link != null) {
836                x = styleCell(tr.td(), rowCounter == 0, true, 5).attributeNN("title", cell.display).rowspan(c).ah(link);
837              } else {
838                x = styleCell(tr.td(), rowCounter == 0, true, 5).attributeNN("title", cell.display).rowspan(c);
839              }
840//              if (cell.clone != null) {
841//                x.style("color: grey");
842//              }
843              x.tx(cell.present());
844            }
845          }
846          first = false;
847        } else {
848          if (!cell.renderedRel) { 
849            int c = 1;
850            for (int i = rowCounter + 1; i < rows.rowSets.size(); i++) {
851              if ((cell.relationship != null && rows.rowSets.get(i).cells.size() > cellCounter && cell.relationship.equals(rows.rowSets.get(i).cells.get(cellCounter).relationship)) && 
852                  (cell.code != null && cell.code.equals(rows.rowSets.get(i).cells.get(cellCounter).code)) && 
853                  (last.code != null && cell.code.equals(rows.rowSets.get(i).cells.get(cellCounter-1).code))) {
854                rows.rowSets.get(i).cells.get(cellCounter).renderedRel = true;
855                c++;
856              } else {
857                break;
858              }
859            }
860            if (last.code == null || cell.code == null) {
861              styleCell(tr.td(), rowCounter == 0, true, 5).style("background-color: #eeeeee");
862            } else if (cell.relationship != null) {
863              styleCell(tr.tdW(16), rowCounter == 0, true, 0).attributeNN("title", cell.relComment).rowspan(c).style("background-color: LightGrey; text-align: center; vertical-align: middle; color: white").tx(cell.relationship);
864            } else {
865              styleCell(tr.tdW(16), rowCounter == 0, false, 0).rowspan(c);
866            }
867          }
868          if (!cell.renderedCode) { 
869            int c = 1;
870            for (int i = rowCounter + 1; i < rows.rowSets.size(); i++) {
871              if (cell.code != null && rows.rowSets.get(i).cells.size() > cellCounter && cell.code.equals(rows.rowSets.get(i).cells.get(cellCounter).code)) {
872                rows.rowSets.get(i).cells.get(cellCounter).renderedCode = true;
873                c++;
874              } else {
875                break;
876              }
877            }
878            if (cell.code == null) {
879              styleCell(tr.td(), rowCounter == 0, true, 5).rowspan(c).style("background-color: #eeeeee");
880            } else {
881              String link = advisor.getLink(rmmContext, cell.system, cell.code);
882              XhtmlNode x = null;
883              if (link != null) {
884                x = styleCell(tr.td(), rowCounter == 0, true, 5).attributeNN("title", cell.display).rowspan(c).ah(link);
885              } else {
886                x = styleCell(tr.td(), rowCounter == 0, true, 5).attributeNN("title", cell.display).rowspan(c);                
887              }
888//              if (cell.clone != null) {
889//                x.style("color: grey");
890//              }
891              x.tx(cell.present());
892            }
893          }
894        }
895        last = cell;
896        cellCounter++;
897      }
898      rowCounter++;
899    }    
900  }
901
902  private static XhtmlNode styleCell(XhtmlNode td, boolean firstrow, boolean sides, int padding) {
903    if (firstrow) {
904      td.style("vertical-align: middle; border-top: 1px solid black; padding: "+padding+"px");
905    } else {
906      td.style("vertical-align: middle; border-top: 1px solid LightGrey; padding: "+padding+"px");
907    }
908    if (sides) {
909      td.style("border-left: 1px solid LightGrey; border-right: 2px solid LightGrey");
910    }
911    return td;
912  }
913
914  private static void populateRows(List<MultipleMappingRow> rowSets, ConceptMap map, int i, IMultiMapRendererAdvisor advisor, Object rmmContext) {
915    // if we can resolve the value set, we create entries for it
916    if (map.hasSourceScope()) {
917      List<Coding> codings = advisor.getMembers(rmmContext, map.getSourceScope().primitiveValue());
918      if (codings != null) {
919        for (Coding c : codings) {
920          MultipleMappingRow row = i == 0 ? null : findExistingRowBySource(rowSets, c.getSystem(), c.getCode(), i);
921          if (row == null) {
922            row = new MultipleMappingRow(i, c.getSystem(), c.getCode(), c.getDisplay());
923            rowSets.add(row);
924          } 
925          
926        }
927      }
928    }  
929    
930    for (ConceptMapGroupComponent grp : map.getGroup()) {
931      for (SourceElementComponent src : grp.getElement()) {
932        MultipleMappingRow row = findExistingRowBySource(rowSets, grp.getSource(), src.getCode(), i);
933        if (row == null) {
934          row = new MultipleMappingRow(i, grp.getSource(), src.getCode(), src.getDisplay());
935          rowSets.add(row);
936        } 
937        if (src.getNoMap()) {
938          row.addTerminus();
939        } else {
940          List<TargetElementComponent> todo = new ArrayList<>();
941          for (TargetElementComponent tgt : src.getTarget()) {
942            MultipleMappingRow trow = findExistingRowByTarget(rowSets, grp.getTarget(), tgt.getCode(), i);
943            if (trow == null) {
944              row.addTarget(grp.getTarget(), tgt.getCode(), tgt.getRelationship(), tgt.getComment(), rowSets, i);
945            } else {
946              todo.add(tgt);
947            }
948          }
949          // we've already got a mapping to these targets. So we gather them under the one mapping - but do this after the others are done
950          for (TargetElementComponent t : todo) {
951            MultipleMappingRow trow = findExistingRowByTarget(rowSets, grp.getTarget(), t.getCode(), i);     
952            if (row.alreadyHasMappings(i)) {
953              // src is already mapped, and so is target, and now we need to map src to target too
954              // we have to clone src, but we only clone the last
955              trow.cloneSource(i, row.getLastSource(i));
956            } else {
957              trow.addSource(row, rowSets, t.getRelationship(), t.getComment());
958            }
959          }
960        }
961      }
962      boolean copy = grp.hasUnmapped() && grp.getUnmapped().getMode() == ConceptMapGroupUnmappedMode.USESOURCECODE;
963      if (copy) {
964        for (MultipleMappingRow row : rowSets) {
965          if (row.rowSets.get(0).cells.size() == i && row.lastSystem().equals(grp.getSource())) {
966            row.addCopy(grp.getTarget());
967          }
968        }
969      }
970    } 
971    for (MultipleMappingRow row : rowSets) {
972      if (row.rowSets.get(0).cells.size() == i) {
973        row.addTerminus();
974      }
975    }
976    if (map.hasTargetScope()) {
977      List<Coding> codings = advisor.getMembers(rmmContext, map.getTargetScope().primitiveValue());
978      if (codings != null) {
979        for (Coding c : codings) {
980          MultipleMappingRow row = findExistingRowByTarget(rowSets, c.getSystem(), c.getCode(), i);
981          if (row == null) {
982            row = new MultipleMappingRow(i+1, c.getSystem(), c.getCode(), c.getDisplay());
983            rowSets.add(row);
984          } else {
985            for (MultipleMappingRowItem cells : row.rowSets) {
986              Cell last = cells.cells.get(cells.cells.size() -1);
987              if (last.system != null && last.system.equals(c.getSystem()) && last.code.equals(c.getCode()) && last.display == null) {
988                last.display = c.getDisplay();
989              }
990            }
991          }
992        }
993      }
994    }
995
996  }
997
998  private static MultipleMappingRow findExistingRowByTarget(List<MultipleMappingRow> rows, String system, String code, int i) {
999    for (MultipleMappingRow row : rows) {
1000      for (MultipleMappingRowItem cells : row.rowSets) {
1001        if (cells.cells.size() > i + 1 && cells.cells.get(i+1).matches(system, code)) {
1002          return row;
1003        }
1004      }
1005    }
1006    return null;
1007  }
1008
1009  private static MultipleMappingRow findExistingRowBySource(List<MultipleMappingRow> rows, String system, String code, int i) {
1010    for (MultipleMappingRow row : rows) {
1011      for (MultipleMappingRowItem cells : row.rowSets) {
1012        if (cells.cells.size() > i && cells.cells.get(i).matches(system, code)) {
1013          return row;
1014        }
1015      }
1016    }
1017    return null;
1018  }
1019}