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