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