001package org.hl7.fhir.r5.renderers;
002
003import java.io.IOException;
004import java.io.UnsupportedEncodingException;
005import java.util.ArrayList;
006import java.util.Base64;
007import java.util.HashMap;
008import java.util.List;
009import java.util.Map;
010
011import org.hl7.fhir.exceptions.DefinitionException;
012import org.hl7.fhir.exceptions.FHIRException;
013import org.hl7.fhir.exceptions.FHIRFormatError;
014import org.hl7.fhir.r5.model.StructureDefinition;
015import org.hl7.fhir.r5.renderers.utils.RenderingContext;
016import org.hl7.fhir.r5.renderers.utils.ResourceWrapper;
017import org.hl7.fhir.r5.utils.EOperationOutcome;
018import org.hl7.fhir.utilities.FileUtilities;
019import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage;
020import org.hl7.fhir.utilities.Utilities;
021import org.hl7.fhir.utilities.filesystem.ManagedFileAccess;
022import org.hl7.fhir.utilities.xhtml.XhtmlNode;
023
024@MarkedToMoveToAdjunctPackage
025public class PatientRenderer extends ResourceRenderer {
026
027
028  public PatientRenderer(RenderingContext context) { 
029    super(context); 
030  } 
031
032
033  @Override
034  public String buildSummary(ResourceWrapper pat) throws UnsupportedEncodingException, IOException {
035    ResourceWrapper id = null;
036    List<ResourceWrapper> list = pat.children("identifier");
037    for (ResourceWrapper t : list) {
038      id = chooseId(id, t);
039    }
040    list = pat.children("name");
041    ResourceWrapper n = null;
042    for (ResourceWrapper t : list) {
043      n = chooseName(n, t);
044    }
045    String gender = null;
046    ResourceWrapper item = pat.child("gender");
047    if (item != null) {
048      gender = context.getTranslatedCode(item.primitiveValue(), "http://hl7.org/fhir/administrative-gender");
049    }
050    ResourceWrapper dt = pat.child("birthDate"); 
051
052    StringBuilder b = new StringBuilder();
053    if (n != null) {
054      b.append(displayHumanName(n));
055    } else {
056      b.append(context.formatPhrase(RenderingContext.PAT_NO_NAME));      
057    }
058    b.append(" ");
059    if (item == null) {
060      b.append(context.formatPhrase(RenderingContext.PAT_NO_GENDER));
061    } else {
062      b.append(gender);
063    }
064    b.append(", ");
065    if (dt == null) {
066      b.append(context.formatPhrase(RenderingContext.PAT_NO_DOB));
067    } else {
068      b.append(context.formatPhrase(RenderingContext.PAT_DOB, displayDateTime(dt)));      
069    }
070    if (id != null) {
071      b.append(" ( ");      
072      b.append(displayIdentifier(id));
073      b.append(")");      
074    }
075    return b.toString();
076  }
077
078
079  //  // name gender DoB (MRN)
080  //  public String display(Resource dr) {
081  //    Patient pat = (Patient) dr;
082  //    Identifier id = null;
083  //    for (Identifier t : pat.getIdentifier()) {
084  //      id = chooseId(id, t);
085  //    }
086  //    HumanName n = null;
087  //    for (HumanName t : pat.getName()) {
088  //      n = chooseName(n, t);
089  //    }
090  //    return display(n, pat.hasGender() ? context.getTranslatedCode(pat.getGenderElement(), "http://hl7.org/fhir/administrative-gender") : null, pat.getBirthDateElement(), id);
091  //  }
092
093
094  private static final int MAX_IMAGE_LENGTH = 2*1024*1024;
095  private static final boolean SHORT = false;
096
097
098  @Override
099  public void buildNarrative(RenderingStatus status, XhtmlNode x, ResourceWrapper pat) throws FHIRFormatError, DefinitionException, IOException, FHIRException, EOperationOutcome {
100    renderResourceTechDetails(pat, x);
101    if (context.isShortPatientForm()) {
102      ResourceWrapper id = null;
103      List<ResourceWrapper> list = pat.children("identifier");
104      for (ResourceWrapper t : list) {
105        id = chooseId(id, t);
106      }
107      list = pat.children("name");
108      ResourceWrapper n = null;
109      for (ResourceWrapper t : list) {
110        n = chooseName(n, t);
111      }
112      String gender = null;
113      ResourceWrapper item = pat.child("gender");
114      if (item != null) {
115        gender = getTranslatedCode(item);
116      }
117      ResourceWrapper dt = pat.child("birthDate");
118
119      if (n == null) {
120        x.b().tx(context.formatPhrase(RenderingContext.PAT_NO_NAME)); // todo: is this appropriate?  
121      } else {
122        renderDataType(status, xlinkNarrative(x.b(), n), n);
123      }
124      x.tx(" ");
125      if (gender == null) {
126        x.tx(context.formatPhrase(RenderingContext.PAT_NO_GENDER));
127      } else {
128        spanIfTracking(x, pat.child("gender")).tx(gender);
129      }
130      x.tx(", ");
131      if (dt == null) {
132        x.tx(context.formatPhrase(RenderingContext.PAT_NO_DOB));
133      } else {
134        spanIfTracking(x, dt).tx(context.formatPhrase(RenderingContext.PAT_DOB, displayDateTime(dt)));
135      }
136      if (id != null) {
137        x.tx(" ( ");      
138        renderDataType(status, spanIfTracking(x, id), id);
139        x.tx(")");      
140      }
141    } else {
142      // banner
143      makeBanner(x.para(), pat).tx(buildSummary(pat));
144      x.hr();
145      XhtmlNode tbl;
146      if (hasRenderablePhoto(pat)) {
147        tbl = x.table(null, true);
148        XhtmlNode tr = tbl.tr();
149        tbl = tr.td().table("grid", false);
150        renderPhoto(tr.td(), pat);
151      } else {
152        tbl = x.table("grid", false);
153      }
154
155      // the table has 4 columns
156      addStatus(status, tbl, pat);
157      addIdentifiers(status, tbl, pat);
158      addNames(status, tbl, pat);
159      addComms(status, tbl, pat);
160      addLangs(status, tbl, pat);
161      addNOKs(status, tbl, pat);
162      addLinks(status, tbl, pat);
163      addExtensions(status, tbl, pat);
164      if (tbl.isEmpty()) {
165        x.remove(tbl);
166      }
167      if (pat.has("contained") && context.isTechnicalMode()) {
168        x.hr();
169        x.para().b().tx(context.formatMessagePlural(pat.children("contained").size(), RenderingContext.PAT_CONTAINED));
170        addContained(status, x, pat.children("contained"));
171      }
172    }
173  }
174
175  private ResourceWrapper chooseId(ResourceWrapper oldId, ResourceWrapper newId) {
176    if (oldId == null) {
177      return newId;
178    }
179    if (newId == null) {
180      return oldId;
181    }
182    return isPreferredId(newId.primitiveValue("use"), oldId.primitiveValue("use")) ? newId : oldId;
183  }
184
185  private boolean isPreferredId(String newUse, String oldUse) {
186    if (newUse == null && oldUse == null || newUse == oldUse) {
187      return false;
188    }
189    if (newUse == null) {
190      return true;
191    }
192    switch (newUse) {
193    case "official": return !Utilities.existsInList(oldUse, "usual");
194    case "old": return !Utilities.existsInList(oldUse, "official", "secondary", "usual");
195    case "secondary": return !Utilities.existsInList(oldUse, "official", "usual");
196    case "temp": return !Utilities.existsInList(oldUse, "official", "secondary", "usual");
197    case "usual": return true;
198    default: return false;
199    }
200  }
201
202  private ResourceWrapper chooseName(ResourceWrapper oldName, ResourceWrapper newName) {
203    if (oldName == null) {
204      return newName;
205    }
206    if (newName == null) {
207      return oldName;
208    }
209    return isPreferredName(newName.primitiveValue("use"), oldName.primitiveValue("use")) ? newName : oldName;
210  }
211
212
213  private boolean isPreferredName(String newUse, String oldUse) {
214    if (newUse == null && oldUse == null || newUse == oldUse) {
215      return false;
216    }
217    if (newUse == null) {
218      return true;
219    }
220    if (oldUse == null) {
221      return Utilities.existsInList(newUse, "official", "usual");
222    }
223    switch (oldUse) {
224    case "anonymous": return Utilities.existsInList(newUse, "official", "usual");
225    case "maiden": return Utilities.existsInList(newUse, "official", "usual");
226    case "nickname": return Utilities.existsInList(newUse, "official", "usual");
227    case "official": return Utilities.existsInList(newUse, "usual");
228    case "old": return Utilities.existsInList(newUse, "official", "usual");
229    case "temp": return Utilities.existsInList(newUse, "official", "usual");
230    case "usual": return false; 
231    }
232    return false;
233  }
234
235  private void addExtensions(RenderingStatus status, XhtmlNode tbl, ResourceWrapper r) throws UnsupportedEncodingException, FHIRException, IOException {
236    Map<String, List<ResourceWrapper>> extensions = new HashMap<>();
237    List<ResourceWrapper> pw = r.children("extension");
238    for (ResourceWrapper t : pw) {  
239      String url = t.primitiveValue("url");
240      if (!extensions.containsKey(url)) {
241        extensions.put(url, new ArrayList<>());
242      }
243      extensions.get(url).add(t);
244    }
245
246    for (String url : extensions.keySet()) {
247      StructureDefinition sd = findCanonical(StructureDefinition.class, url, r);
248      if (sd != null) {
249        List<ResourceWrapper> list = extensions.get(url);
250        boolean anyComplex = false;
251        for (ResourceWrapper ext : list) {
252          anyComplex = anyComplex || ext.has("extension");
253        }
254        if (!anyComplex) {
255          XhtmlNode tr = tbl.tr();
256          nameCell(tr, getContext().getTranslated(sd.getTitleElement()), sd.getDescription(), sd.getWebPath());
257          XhtmlNode td = tr.td();
258          td.colspan("3");
259          if (list.size() != 1) {
260            XhtmlNode ul = td.ul();
261            for (ResourceWrapper s : list) {
262              XhtmlNode li = ul.li();
263              renderDataType(status, xlinkNarrative(li, s.child("value")), s.child("value"));
264            }
265          } else {
266            renderDataType(status, xlinkNarrative(td, list.get(0).child("value")), list.get(0).child("value"));
267          }
268        } else {
269          for (ResourceWrapper ext : list) {
270            XhtmlNode tr = tbl.tr();
271            nameCell(tr, sd.getTitle()+":", sd.getDescription());
272            XhtmlNode td = tr.td();
273            td.colspan("3");
274            if (ext.has("extension")) {
275              XhtmlNode ul = td.ul();
276              for (ResourceWrapper s : ext.extensions()) {
277                XhtmlNode li = ul.li();
278                li.tx(s.primitiveValue("url")+": ");
279                if (s.has("extension")) {
280                  boolean first = true;
281                  for (ResourceWrapper t : s.extensions()) {
282                    if (first) first = false; else li.tx("; ");
283                    li.tx(t.primitiveValue("url")+"=");
284                    renderDataType(status, xlinkNarrative(li, t.child("value")), t.child("value"));
285                  }
286                } else {
287                  renderDataType(status, xlinkNarrative(li, s.child("value")), s.child("value"));
288                }
289              }
290            } else {
291              renderDataType(status, xlinkNarrative(td, ext.child("value")), ext.child("value"));
292            }
293          }
294        }
295      }
296    }
297
298
299  }
300
301  private void addIdentifiers(RenderingStatus status, XhtmlNode tbl, ResourceWrapper r) throws FHIRFormatError, DefinitionException, IOException {
302    List<ResourceWrapper> ids = r.children("identifier");
303    ResourceWrapper id = null;
304    for (ResourceWrapper i : ids) {
305      id = chooseId(id, i);
306    }
307    if (id != null) {
308      ids.remove(id);
309    };
310    if (ids.size() > 0) {
311      XhtmlNode tr = tbl.tr();
312      nameCell(tr, context.formatMessagePlural(ids.size(), RenderingContext.PAT_OTHER_ID),context.formatMessagePlural(ids.size(), RenderingContext.PAT_OTHER_ID_HINT));
313      XhtmlNode td = tr.td();
314      td.colspan("3");
315      if (ids.size() == 1) {
316        renderDataType(status, xlinkNarrative(td, ids.get(0)), ids.get(0));
317      } else { 
318        XhtmlNode ul = td.ul();
319        for (ResourceWrapper i : ids) {
320          renderDataType(status, xlinkNarrative(ul.li(), i), i);
321        }
322      }
323    }
324  }
325
326  private void addLangs(RenderingStatus status, XhtmlNode tbl, ResourceWrapper r) throws FHIRFormatError, DefinitionException, IOException {
327    List<ResourceWrapper> langs = new ArrayList<ResourceWrapper>();
328    List<ResourceWrapper> comms = r.children("communication");
329    ResourceWrapper prefLang = null;
330    for (ResourceWrapper t : comms) {
331      ResourceWrapper lang = t.child("language");
332      if (lang != null) {
333        langs.add(lang);
334        ResourceWrapper l = t.child("preferred");
335        if (l != null && "true".equals(l.primitiveValue())) {
336          prefLang = lang;
337        }
338      }
339    }
340    if (langs.size() > 0) {
341      XhtmlNode tr = tbl.tr();
342      nameCell(tr, context.formatMessagePlural(langs.size(), RenderingContext.PAT_LANG), context.formatMessagePlural(langs.size(), RenderingContext.PAT_LANG_HINT));
343      XhtmlNode td = tr.td();
344      td.colspan("3");
345      if (langs.size() == 1) {
346        renderDataType(status, xlinkNarrative(td, langs.get(0)), langs.get(0));
347        if (prefLang != null) {
348          td.tx(" "+context.formatPhrase(RenderingContext.PAT_LANG_PREFERRED));
349        }
350      } else if (langs.size() > 1) {
351        XhtmlNode ul = td.ul();
352        for (ResourceWrapper i : langs) {
353          XhtmlNode li = ul.li();
354          renderDataType(status, xlinkNarrative(li, i), i);
355          if (i == prefLang) {
356            li.tx(" "+context.formatPhrase(RenderingContext.PAT_LANG_PREFERRED));;
357          }
358        }
359      }
360    }
361  }
362
363
364
365  public class NamedReferance {
366
367    private String name;
368    private ResourceWrapper type;
369    private ResourceWrapper reference;
370
371    public NamedReferance(String name, ResourceWrapper type, ResourceWrapper ref) {
372      this.name = name;
373      this.type = type;
374      this.reference = ref;
375    }
376
377    public String getName() {
378      return name;
379    }
380
381    public ResourceWrapper getReference() {
382      return reference;
383    }
384
385    public ResourceWrapper getType() {
386      return type;
387    }
388
389  }
390
391
392  private void addLinks(RenderingStatus status, XhtmlNode tbl, ResourceWrapper r) throws UnsupportedEncodingException, FHIRException, IOException {
393    List<NamedReferance> refs = new ArrayList<>();
394    List<ResourceWrapper> pw = r.children("generalPractitioner");
395    for (ResourceWrapper t : pw) {
396      refs.add(new NamedReferance(context.formatPhrase(RenderingContext.PAT_GP), null, t));
397    }
398    pw = r.children("managingOrganization");
399    for (ResourceWrapper t : pw) {
400      refs.add(new NamedReferance(context.formatPhrase(RenderingContext.PAT_MO), null, t));
401    }
402    pw = r.children("link");
403    for (ResourceWrapper t : pw) {
404      ResourceWrapper o = t.firstChild("other");
405      ResourceWrapper l = t.firstChild("type");
406      if (l != null && o != null) {
407        refs.add(new NamedReferance(describeLinkedRecord(l.primitiveValue()), l,   o));        
408      }
409    }
410
411    if (refs.size() > 0) {      
412      XhtmlNode tr = tbl.tr();
413      nameCell(tr, context.formatPhrase(RenderingContext.PAT_LINKS), context.formatPhrase(RenderingContext.PAT_LINKS_HINT));
414      XhtmlNode td = tr.td();
415      td.colspan("3");
416      XhtmlNode ul = td.ul();
417      for (NamedReferance ref : refs) {
418        XhtmlNode li = ul.li();
419        if (ref.getType() != null) {
420          spanIfTracking(li, ref.getType()).tx(ref.getName());
421        } else {
422          li.tx(ref.getName());
423        }
424        li.tx(": ");
425        renderReference(status, li, ref.getReference());        
426      }
427    }
428  }
429
430  private String describeLinkedRecord(String type) {
431    switch (type) {
432    case "replaced-by" : return context.formatPhrase(RenderingContext.PAT_LINK_REPLBY);
433    case "replaces": return context.formatPhrase(RenderingContext.PAT_LINK_REPL);
434    case "refer": return context.formatPhrase(RenderingContext.PAT_LINK_REFER);
435    case "seealso": return context.formatPhrase(RenderingContext.PAT_LINK_SEE);
436    }
437    return "Unknown";
438  }
439
440  private void addNOKs(RenderingStatus status, XhtmlNode tbl, ResourceWrapper r) throws FHIRFormatError, DefinitionException, IOException {
441    for (ResourceWrapper t : r.children("contact")) {
442      addNOK(status, tbl, r,  t);
443    }
444  }
445
446  private void addNOK(RenderingStatus status, XhtmlNode tbl, ResourceWrapper r, ResourceWrapper bw) throws FHIRFormatError, DefinitionException, IOException {
447    List<ResourceWrapper> rels = bw.children("relationship");
448    ResourceWrapper name = bw.firstChild("name");
449    ResourceWrapper add = bw.firstChild("address");
450    String gender = context.getTranslatedCode(bw.primitiveValue("gender"), "http://hl7.org/fhir/administrative-gender");
451    ResourceWrapper period = bw.firstChild("period");
452    ResourceWrapper organization = bw.firstChild("organization");
453    List<ResourceWrapper> tels = bw.children("telecom");
454
455    if (rels.size() < 2 && name == null && add == null && gender == null && period == null && organization == null && tels.size() == 0) {
456      return; // nothing to render 
457    }
458    XhtmlNode tr = tbl.tr();
459    if (rels.size() == 1) {
460      nameCell(tr, displayDataType(rels.get(0))+":",  context.formatPhrase(RenderingContext.PAT_NOM_CONTACT)+" "+displayDataType(rels.get(0)));
461    } else {
462      nameCell(tr, context.formatPhrase(RenderingContext.GENERAL_CONTACT), context.formatPhrase(RenderingContext.PAT_NOK_CONTACT_HINT));
463    }
464    XhtmlNode td = tr.td();
465    td.colspan("3");
466    XhtmlNode ul = td.ul();
467    XhtmlNode li;
468    if (name != null) {
469      li = ul.li();
470      renderDataType(status, xlinkNarrative(li, name), name);
471      if (gender != null) {
472        li.tx(" "+"("+gender+")");
473      }
474    } else if (gender != null) {
475      li = ul.li();
476      li.tx(context.formatPhrase(RenderingContext.PAT_GENDER, gender));      
477    }
478    if (rels.size() > 1) {
479      li = ul.li();
480      li.tx(context.formatPhrase(RenderingContext.PAT_RELN));
481      boolean first = true;
482      for (ResourceWrapper rel : rels) {
483        if (first) first = false; else li.tx(", ");
484        renderDataType(status, xlinkNarrative(li, rel), rel);
485      }      
486    }
487    if (add != null) {
488      renderDataType(status, xlinkNarrative(ul.li(), add), add);
489    }
490    for (ResourceWrapper cp : tels) {
491      renderDataType(status, xlinkNarrative(ul.li(), cp), cp);
492    }
493    if (organization != null) {
494      li = ul.li();
495      li.tx(context.formatPhrase(RenderingContext.PAT_ORG));
496      renderDataType(status, xlinkNarrative(li, organization), organization);
497    }
498    if (period != null) {
499      li = ul.li();
500      li.tx(context.formatPhrase(RenderingContext.PAT_PERIOD));
501      renderDataType(status, xlinkNarrative(li, period), period);
502    }
503  }
504
505  private void addNames(RenderingStatus status, XhtmlNode tbl, ResourceWrapper r) throws FHIRFormatError, DefinitionException, IOException {
506    List<ResourceWrapper> names = r.children("name");
507    ResourceWrapper name = null;
508    for (ResourceWrapper n : names) {
509      name = chooseName(name, n);
510    }
511    if (name != null) {
512      names.remove(name);
513    };
514    if (names.size() == 1) {
515      XhtmlNode tr = tbl.tr();
516      nameCell(tr, context.formatPhrase(RenderingContext.PAT_ALT_NAME), context.formatPhrase(RenderingContext.PAT_ALT_NAME_HINT));
517      XhtmlNode td = tr.td();
518      td.colspan("3");
519      if (names.size() == 1) {
520        renderDataType(status, xlinkNarrative(td, names.get(0)), names.get(0));
521      } else {
522        XhtmlNode ul = td.ul();
523        for (ResourceWrapper n : names) {
524          renderDataType(status,xlinkNarrative(ul.li(), n), n);
525        }
526      }
527    }
528  }
529
530  private void addComms(RenderingStatus status, XhtmlNode tbl, ResourceWrapper r) throws FHIRFormatError, DefinitionException, IOException {
531    List<ResourceWrapper> tels = r.children("telecom");
532    List<ResourceWrapper> adds = r.children("address");
533    if (tels.size() + adds.size() > 0) {
534      XhtmlNode tr = tbl.tr();
535      nameCell(tr, context.formatPhrase(RenderingContext.PAT_CONTACT), context.formatPhrase(RenderingContext.PAT_CONTACT_HINT));
536      XhtmlNode td = tr.td();
537      td.colspan("3");
538      if (tels.size() + adds.size() == 1) {
539        if (adds.isEmpty()) {
540          renderDataType(status, xlinkNarrative(td, tels.get(0)), tels.get(0));
541        } else {
542          renderDataType(status, xlinkNarrative(td, adds.get(0)), adds.get(0));
543        }
544      } else {
545        XhtmlNode ul = td.ul();
546        for (ResourceWrapper n : tels) {
547          renderDataType(status, xlinkNarrative(ul.li(), n), n);
548        }
549        for (ResourceWrapper n : adds) {
550          renderDataType(status, xlinkNarrative(ul.li(), n), n);
551        }
552      }
553    }
554  }
555
556  private void addStatus(RenderingStatus status, XhtmlNode tbl, ResourceWrapper r) throws FHIRFormatError, DefinitionException, UnsupportedEncodingException, FHIRException, IOException {
557    // TODO Auto-generated method stub
558    int count = 0;
559    if (r.has("active")) {
560      count++;
561    }
562    if (r.has("deceased")) {
563      count++;
564    }
565    if (r.has("maritalStatus")) {
566      count++;
567    }
568    if (r.has("multipleBirth")) {
569      count++;
570    }
571    if (count > 0) {
572      XhtmlNode tr = tbl.tr();
573      int pos = 0;
574      if (r.has("active")) {
575        List<ResourceWrapper> a = r.children("active");
576        if (!a.isEmpty()) {
577          pos++;
578          nameCell(tr, context.formatPhrase(RenderingContext.PAT_ACTIVE), context.formatPhrase(RenderingContext.PAT_ACTIVE_HINT));
579          XhtmlNode td = tr.td();
580          if (pos == count) {
581            td.colspan("3");
582          }
583          renderDataType(status, xlinkNarrative(td, a.get(0)), a.get(0));
584        }
585      }      
586      if (r.has("deceased[x]")) {
587        List<ResourceWrapper> a = r.children("deceased[x]");
588        if (!a.isEmpty()) {
589          pos++;
590          nameCell(tr, context.formatPhrase(RenderingContext.PAT_DECEASED), context.formatPhrase(RenderingContext.PAT_DECEASED_HINT));
591          XhtmlNode td = tr.td();
592          if (pos == count) {
593            td.colspan("3");
594          }
595          renderDataType(status, xlinkNarrative(td, a.get(0)), a.get(0));
596        }
597      }      
598      if (r.has("maritalStatus")) {
599        List<ResourceWrapper> a = r.children("maritalStatus");
600        if (!a.isEmpty()) {
601          pos++;
602          if (pos == 3) {
603            tr = tbl.tr();          
604          }
605          nameCell(tr, context.formatPhrase(RenderingContext.PAT_MARITAL), context.formatPhrase(RenderingContext.PAT_MARITAL_HINT));
606          XhtmlNode td = tr.td();
607          if (pos == count) {
608            td.colspan("3");
609          }
610          renderDataType(status, xlinkNarrative(td, a.get(0)), a.get(0));
611        }
612      }      
613      if (r.has("multipleBirth[x]")) {
614        List<ResourceWrapper> a = r.children("multipleBirth[x]");
615        if (!a.isEmpty()) {
616          pos++;
617          if (pos == 3) {
618            tr = tbl.tr();          
619          }
620          nameCell(tr, context.formatPhrase(RenderingContext.PAT_MUL_BIRTH), context.formatPhrase(RenderingContext.PAT_MUL_BIRTH_HINT));
621          XhtmlNode td = tr.td();
622          if (pos == count) {
623            td.colspan("3");
624          }
625          renderDataType(status, xlinkNarrative(td, a.get(0)), a.get(0));
626        }
627      }      
628    }  
629  }
630
631  private void nameCell(XhtmlNode tr, String text, String title) {
632    XhtmlNode td = tr.td();
633    td.setAttribute("title", title);
634    td.tx(text);
635    td.style("background-color: #f3f5da");
636    markBoilerplate(td);
637  }
638
639  private void nameCell(XhtmlNode tr, String text, String title, String link) {
640    XhtmlNode td = tr.td();
641    td.setAttribute("title", title);
642    if (link != null) {
643      td.ah(context.prefixLocalHref(link)).tx(text); 
644    } else {
645      td.tx(text);
646    }
647    td.style("background-color: #f3f5da");
648    markBoilerplate(td);
649  }
650
651  private void renderPhoto(XhtmlNode td, ResourceWrapper r) throws UnsupportedEncodingException, FHIRException, IOException {
652    if (r.has("photo")) {
653      List<ResourceWrapper> a = r.children("photo");
654      for (ResourceWrapper att : a) {
655        String ct = att.primitiveValue("contentType");
656        byte[] cnt = att.has("data") ? Base64.getDecoder().decode(att.primitiveValue("data")) : null;
657        if (ct.startsWith("image/") &&
658            cnt != null && (!context.isInlineGraphics() || (cnt.length > 0 && cnt.length < MAX_IMAGE_LENGTH))) {
659          String ext = extensionForType(ct);
660          if (context.isInlineGraphics() || Utilities.noString(context.getDestDir()) || ext == null) {
661            td.img("data:"+ct+";base64,"+att.primitiveValue("data"), "patient photo");
662          } else {
663            String n = context.getRandomName(r.getId())+ext;
664            FileUtilities.bytesToFile(cnt, ManagedFileAccess.file(Utilities.path(context.getDestDir(), n)));
665            context.registerFile(n);
666            td.img(n, context.formatPhrase(RenderingContext.PAT_PHOTO));            
667          }
668          return;
669        } 
670      }
671    }      
672    return;
673  }
674
675  private String extensionForType(String contentType) {
676    if (contentType.equals("image/gif")) {
677      return ".gif";
678    }
679    if (contentType.equals("image/png")) {
680      return ".png";
681    }
682    if (contentType.equals("image/jpeg")) {
683      return ".jpg";
684    }
685    return null;
686  }
687
688  private boolean hasRenderablePhoto(ResourceWrapper r) throws UnsupportedEncodingException, FHIRException, IOException {
689    if (r.has("photo")) {
690      List<ResourceWrapper> a = r.children("photo");
691      for (ResourceWrapper att : a) {
692        if (att.has("contentType") && att.primitiveValue("contentType").startsWith("image/") &&
693            att.has("data") && (!context.isInlineGraphics() || (att.primitiveValue("data").length() > 0 && 
694                att.primitiveValue("data").length() < MAX_IMAGE_LENGTH))) {
695          return true;
696        } 
697      }
698    }      
699    return false;
700  }
701
702  private XhtmlNode makeBanner(XhtmlNode para, ResourceWrapper res) {
703    para.style("border: 1px #661aff solid; background-color: #e6e6ff; padding: 10px;");
704    xlinkNarrative(para, res);
705    return para;
706  }
707}