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