ctakes-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From seanfi...@apache.org
Subject svn commit: r1825907 [1/4] - in /ctakes/trunk: ctakes-core/src/main/java/org/apache/ctakes/core/ae/ ctakes-core/src/main/java/org/apache/ctakes/core/cc/html/ ctakes-core/src/main/java/org/apache/ctakes/core/cr/ ctakes-core/src/main/java/org/apache/ctak...
Date Mon, 05 Mar 2018 15:09:59 GMT
Author: seanfinan
Date: Mon Mar  5 15:09:58 2018
New Revision: 1825907

URL: http://svn.apache.org/viewvc?rev=1825907&view=rev
Log:
ctakes-494 : Add ae to set subject as family member in family history section
ctakes-495 : Improved html writer
Add abstract file tree reader
Add createSectionName to EssentialAnnotationUtil
Add RelationArgumentUtil
Extracted NoteSpecs from ctakes-fhir
Add SemanticGroup, SemanticTui
ctakes-491 : Full refactoring of improved ctakes-fhir
ctakes-496 : Add NE type constants for previously missing semantic groups

Added:
    ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/ae/SubjectSectionFixer.java
    ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/cc/html/
    ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/cc/html/CssWriter.java
    ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/cc/html/HtmlTextWriter.java
    ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/cc/html/JsWriter.java
    ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/cc/html/SemanticMarkup.java
    ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/cr/AbstractFileTreeReader.java
    ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/note/
    ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/note/NoteSpecs.java
    ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/semantic/
    ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/semantic/SemanticGroup.java
    ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/semantic/SemanticTui.java
    ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/util/RelationArgumentUtil.java
    ctakes/trunk/ctakes-fhir/src/main/java/org/apache/ctakes/fhir/backbone/
    ctakes/trunk/ctakes-fhir/src/main/java/org/apache/ctakes/fhir/backbone/CompositionSectionCreator.java
    ctakes/trunk/ctakes-fhir/src/main/java/org/apache/ctakes/fhir/cc/FhirJsonWriter.java
    ctakes/trunk/ctakes-fhir/src/main/java/org/apache/ctakes/fhir/cr/
    ctakes/trunk/ctakes-fhir/src/main/java/org/apache/ctakes/fhir/cr/FhirXmlFileReader.java
    ctakes/trunk/ctakes-fhir/src/main/java/org/apache/ctakes/fhir/element/
    ctakes/trunk/ctakes-fhir/src/main/java/org/apache/ctakes/fhir/element/FhirElementFactory.java
    ctakes/trunk/ctakes-fhir/src/main/java/org/apache/ctakes/fhir/element/FhirElementParser.java
    ctakes/trunk/ctakes-fhir/src/main/java/org/apache/ctakes/fhir/resource/AnnotationBasicParser.java
    ctakes/trunk/ctakes-fhir/src/main/java/org/apache/ctakes/fhir/resource/CompositionCreator.java
    ctakes/trunk/ctakes-fhir/src/main/java/org/apache/ctakes/fhir/resource/FhirPractitioner.java
    ctakes/trunk/ctakes-fhir/src/main/java/org/apache/ctakes/fhir/resource/FhirResourceParser.java
    ctakes/trunk/ctakes-fhir/src/main/java/org/apache/ctakes/fhir/resource/IdentifiedAnnotationBasicParser.java
    ctakes/trunk/ctakes-fhir/src/main/java/org/apache/ctakes/fhir/resource/SectionBasicParser.java
    ctakes/trunk/ctakes-fhir/src/main/java/org/apache/ctakes/fhir/util/FhirNoteSpecs.java
Modified:
    ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/util/EssentialAnnotationUtil.java
    ctakes/trunk/ctakes-fhir/src/main/java/org/apache/ctakes/fhir/cc/FhirDocComposer.java
    ctakes/trunk/ctakes-fhir/src/main/java/org/apache/ctakes/fhir/resource/AnnotationBasicCreator.java
    ctakes/trunk/ctakes-fhir/src/main/java/org/apache/ctakes/fhir/resource/BundleCreator.java
    ctakes/trunk/ctakes-fhir/src/main/java/org/apache/ctakes/fhir/resource/FhirResourceCreator.java
    ctakes/trunk/ctakes-fhir/src/main/java/org/apache/ctakes/fhir/resource/IdentifiedAnnotationBasicCreator.java
    ctakes/trunk/ctakes-fhir/src/main/java/org/apache/ctakes/fhir/resource/PractitionerCtakes.java
    ctakes/trunk/ctakes-fhir/src/main/java/org/apache/ctakes/fhir/resource/SectionBasicCreator.java
    ctakes/trunk/ctakes-type-system/src/main/java/org/apache/ctakes/typesystem/type/constants/CONST.java

Added: ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/ae/SubjectSectionFixer.java
URL: http://svn.apache.org/viewvc/ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/ae/SubjectSectionFixer.java?rev=1825907&view=auto
==============================================================================
--- ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/ae/SubjectSectionFixer.java (added)
+++ ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/ae/SubjectSectionFixer.java Mon Mar  5 15:09:58 2018
@@ -0,0 +1,54 @@
+package org.apache.ctakes.core.ae;
+
+import org.apache.ctakes.typesystem.type.constants.CONST;
+import org.apache.ctakes.typesystem.type.textsem.IdentifiedAnnotation;
+import org.apache.ctakes.typesystem.type.textspan.Segment;
+import org.apache.log4j.Logger;
+import org.apache.uima.analysis_engine.AnalysisEngineProcessException;
+import org.apache.uima.fit.component.JCasAnnotator_ImplBase;
+import org.apache.uima.fit.util.JCasUtil;
+import org.apache.uima.jcas.JCas;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+/**
+ * @author SPF , chip-nlp
+ * @version %I%
+ * @since 4/11/2017
+ */
+final public class SubjectSectionFixer extends JCasAnnotator_ImplBase {
+
+   static private final Logger LOGGER = Logger.getLogger( "SubjectSectionFixer" );
+
+   static private final Collection<String> FAMILY_HISTORY = Arrays.asList(
+//         "FamilyAndSocialHistory_Section", "FamilyEnviroment_Section", "FamilyMedicalHistory_Section" );
+         "Family Medical History" );
+
+
+   /**
+    * {@inheritDoc}
+    */
+   @Override
+   public void process( final JCas jCas ) throws AnalysisEngineProcessException {
+      LOGGER.info( "Adjusting Subject based upon Section ..." );
+
+      final Collection<Segment> sections = JCasUtil.select( jCas, Segment.class );
+      for ( Segment section : sections ) {
+         if ( !FAMILY_HISTORY.contains( section.getPreferredText() ) ) {
+            continue;
+         }
+         final Collection<IdentifiedAnnotation> annotations
+               = JCasUtil.selectCovered( jCas, IdentifiedAnnotation.class, section );
+         if ( annotations == null ) {
+            continue;
+         }
+         for ( IdentifiedAnnotation annotation : annotations ) {
+            annotation.setSubject( CONST.ATTR_SUBJECT_FAMILY_MEMBER );
+         }
+      }
+      LOGGER.info( "Finished Processing" );
+   }
+
+
+}

Added: ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/cc/html/CssWriter.java
URL: http://svn.apache.org/viewvc/ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/cc/html/CssWriter.java?rev=1825907&view=auto
==============================================================================
--- ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/cc/html/CssWriter.java (added)
+++ ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/cc/html/CssWriter.java Mon Mar  5 15:09:58 2018
@@ -0,0 +1,243 @@
+package org.apache.ctakes.core.cc.html;
+
+
+import org.apache.log4j.Logger;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
+import static org.apache.ctakes.core.cc.html.HtmlTextWriter.*;
+
+
+/**
+ * @author SPF , chip-nlp
+ * @version %I%
+ * @since 10/15/2016
+ */
+final class CssWriter {
+
+   static private final Logger LOGGER = Logger.getLogger( "CssWriter" );
+
+
+   private CssWriter() {
+   }
+
+   /**
+    * @param filePath path to css file
+    */
+   static void writeCssFile( final String filePath ) {
+      final File outputFile = new File( filePath );
+      outputFile.getParentFile()
+            .mkdirs();
+      try ( final BufferedWriter writer = new BufferedWriter( new FileWriter( outputFile ) ) ) {
+         writer.write( setLayout() );
+         writer.write( setUnderline( GENERIC, "gray", "solid", "0.12" ) );
+         writer.write( setUnderline( AFFIRMED, "green", "solid", "0.12" ) );
+         writer.write( setUnderline( UNCERTAIN, "gold", "dotted", "0.16" ) );
+         writer.write( setUnderline( NEGATED, "red", "dashed", "0.14" ) );
+         writer.write( setUnderline( UNCERTAIN_NEGATED, "orange", "dashed", "0.14" ) );
+
+//         writer.write( setSuperColor( SemanticGroup.FINDING.getCode(), "magenta" ) );
+//         writer.write( setSuperColor( SemanticGroup.DISORDER.getCode(), "black" ) );
+//         writer.write( setSuperColor( SemanticGroup.MEDICATION.getCode(), "red" ) );
+//         writer.write( setSuperColor( SemanticGroup.PROCEDURE.getCode(), "blue" ) );
+//         writer.write( setSuperColor( SemanticGroup.ANATOMICAL_SITE.getCode(), "gray" ) );
+//         writer.write( setSuperColor( SemanticGroup.UNKNOWN_SEMANTIC_CODE, "gray" ) );
+         for ( SemanticMarkup markup : SemanticMarkup.values() ) {
+            writer.write( setSuperColor( markup ) );
+         }
+
+//         writer.write( getListCss() );
+         writer.write( getToolTipCss() );
+      } catch ( IOException ioE ) {
+         LOGGER.error( "Could not not write css file " + outputFile.getPath() );
+         LOGGER.error( ioE.getMessage() );
+      }
+   }
+
+   static private String setLayout() {
+      return ".flex-container {\n" +
+            "    display: -webkit-flex;\n" +
+            "    display: flex;  \n" +
+            "    -webkit-flex-flow: row wrap;\n" +
+            "    flex-flow: row wrap;\n" +
+            "    text-align: center;\n" +
+            "}\n" +
+            "\n" +
+            ".flex-container > * {\n" +
+            "    padding: 15px;\n" +
+            "    -webkit-flex: 1 100%;\n" +
+            "    flex: 1 100%;\n" +
+            "}\n" +
+            "\n" +
+            ".article {\n" +
+            "    text-align: left;\n" +
+            "    line-height: 120%;\n" +
+            "    word-spacing: 0.25em;\n" +
+            "}\n" +
+            "header {\n" +
+            "    background: MidnightBlue;\n" +
+            "    color: white;\n" +
+            "    height: 30px;\n" +
+            "}\n" +
+            "header h1 {\n" +
+            "    margin-top: 0px;\n" +
+            "}\n" +
+            "footer {\n" +
+            "    background: SteelBlue;\n" +
+            "    color: white;\n" +
+            "    height: 10px;\n" +
+            "}\n" +
+            ".nav {\n" +
+            "    display: flex;\n" +
+            "    flex-direction: column;\n" +
+            "    flex-shrink: 0;\n" +
+            "    justify-content: space-between;\n" +
+            "    max-width: 320px;\n" +
+            "    background: PowderBlue;\n" +
+            "}\n" +
+            "@media all {\n" +
+            "    .article {\n" +
+            "        -webkit-flex: 5 0px;\n" +
+            "        flex: 5 0px;\n" +
+            "        -webkit-order: 1;\n" +
+            "        order: 1;" +
+            "    }\n" +
+            "    .nav {\n" +
+            "        text-align: left;\n" +
+            "        -webkit-flex: 1 auto;\n" +
+            "        flex: 1 auto;\n" +
+            "        -webkit-order: 2;\n" +
+            "        order: 2;\n" +
+            "    }\n" +
+            "    footer {\n" +
+            "        -webkit-order: 3;\n" +
+            "        order: 3;\n" +
+            "    }\n" +
+            "}\n\n" +
+            "#ia {\n" +
+            "    position: sticky;\n" +
+            "    position: -webkit-sticky;\n" +
+            "    top: 0;\n" +
+            "    background: powderBlue;\n" +
+            "    border-bottom: 2px solid navy;\n" +
+            "    z-index: 10;\n" +
+            "}\n" +
+            ".legend {\n" +
+            "    margin-left: auto;\n" +
+            "    margin-right: auto;\n" +
+            "    max-width: 300px;\n" +
+            "    background: white;\n" +
+            "    border: 2px solid navy;\n" +
+            "    padding: 0 15px 15px 15px;\n" +
+            "    z-index: 1;\n\n" +
+            "}\n" +
+            ".legend h3 {\n" +
+            "    text-align: center;\n" +
+            "}\n" +
+            ".legend table {\n" +
+            "    width: 100%;\n" +
+            "}\n" +
+            ".legend td {\n" +
+            "    width: 50%;\n" +
+            "}\n";
+   }
+
+   // dashType is solid or dashed or double or dotted     size is relative: 0.1 or 0.2 for 10%, 20%
+   // See https://css-tricks.com/styling-underlines-web/ shadow for another possibility
+   static private String setUnderline( final String className, final String color, final String dashType,
+                                       final String size ) {
+      return "\n." + className + " {\n" +
+            "  position: relative;\n" +
+            "  display: inline-block " + color + ";\n" +
+            "  border-bottom: " + size + "em " + dashType + " " + color + ";\n" +
+            "  border-radius: 5px;\n" +
+            "}\n";
+   }
+
+   static private String setSuperColor( final SemanticMarkup markup ) {
+      return "\n." + markup.getEncoding() + " {\n" +
+            "  color: " + markup.getColor() + ";\n" +
+            "}\n" +
+            "." + markup.getEncoding() + "::after {\n" +
+            "  content: \"\\" + markup.getAsterisk() + "\";\n" +
+            "  vertical-align: super;" +
+            "  font-size: smaller;" +
+            "}\n";
+   }
+
+   static private String setSuperColor( final String className, final String color ) {
+      return "\n." + className + " {\n" +
+            "  color: " + color + ";\n" +
+            "}\n";
+   }
+
+   static private String setHighlight( final String idName, final String color ) {
+      // PowderBlue
+      return "#" + idName + "{\n  background-color: " + color + ";\n}\n";
+   }
+
+   static private String getListCss() {
+      return "\nul {\n" +
+            "  list-style-type: none;\n" +
+            "  margin: 0;\n" +
+            "  padding: 0;\n" +
+            "}\n" +
+            "\nli {\n" +
+            "  border: 1px solid lightgray;\n" +
+            "  margin: 1px;\n" +
+            "  margin-right: 5px;\n" +
+            "  padding: 2px;\n" +
+            "  padding-left: 5px;\n" +
+            "}\n";
+   }
+
+   static private String getToolTipCss() {
+      return
+            // position z
+            "\n[" + TOOL_TIP + "] {\n" +
+                  "  position: relative;\n" +
+                  "  z-index: 2;\n" +
+                  "  cursor: pointer;\n" +
+                  "}\n" +
+                  // invisible
+                  "[" + TOOL_TIP + "]::before,\n" +
+                  "[" + TOOL_TIP + "]::after {\n" +
+                  "  visibility: hidden;\n" +
+                  "  -ms-filter: \"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)\";\n" +
+                  "  filter: progid: DXImageTransform.Microsoft.Alpha(Opacity=0);\n" +
+                  "  opacity: 0;\n" +
+                  "  pointer-events: none;\n" +
+                  "}\n" +
+                  // position & sketch
+                  "[" + TOOL_TIP + "]::before {\n" +
+                  "  position: absolute;\n" +
+                  "  bottom: 0%;\n" +
+                  "  left: 100%;\n" +
+                  "  margin-bottom: 5px;\n" +
+                  "  padding: 7px;\n" +
+                  "  -webkit-border-radius: 3px;\n" +
+                  "  -moz-border-radius: 3px;\n" +
+                  "  border-radius: 3px;\n" +
+                  "  background-color: #000;\n" +
+                  "  background-color: hsla(0, 0%, 20%, 0.9);\n" +
+                  "  color: #fff;\n" +
+                  "  content: attr(" + TOOL_TIP + ");\n" +
+                  "  text-align: center;\n" +
+                  "  font-size: 14px;\n" +
+                  "  line-height: 1.2;\n" +
+                  "}\n" +
+                  // hover show
+                  "[" + TOOL_TIP + "]:hover::before,\n" +
+                  "[" + TOOL_TIP + "]:hover::after {\n" +
+                  "  visibility: visible;\n" +
+                  "  -ms-filter: \"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)\";\n" +
+                  "  filter: progid: DXImageTransform.Microsoft.Alpha(Opacity=100);\n" +
+                  "  opacity: 1;\n" +
+                  "}\n";
+   }
+
+
+}

Added: ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/cc/html/HtmlTextWriter.java
URL: http://svn.apache.org/viewvc/ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/cc/html/HtmlTextWriter.java?rev=1825907&view=auto
==============================================================================
--- ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/cc/html/HtmlTextWriter.java (added)
+++ ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/cc/html/HtmlTextWriter.java Mon Mar  5 15:09:58 2018
@@ -0,0 +1,1481 @@
+package org.apache.ctakes.core.cc.html;
+
+
+import org.apache.ctakes.core.cc.AbstractJCasFileWriter;
+import org.apache.ctakes.core.pipeline.PipeBitInfo;
+import org.apache.ctakes.core.semantic.SemanticGroup;
+import org.apache.ctakes.core.semantic.SemanticTui;
+import org.apache.ctakes.core.util.DocumentIDAnnotationUtil;
+import org.apache.ctakes.core.util.OntologyConceptUtil;
+import org.apache.ctakes.core.util.textspan.DefaultTextSpan;
+import org.apache.ctakes.core.util.textspan.TextSpan;
+import org.apache.ctakes.typesystem.type.refsem.Event;
+import org.apache.ctakes.typesystem.type.refsem.EventProperties;
+import org.apache.ctakes.typesystem.type.refsem.UmlsConcept;
+import org.apache.ctakes.typesystem.type.relation.BinaryTextRelation;
+import org.apache.ctakes.typesystem.type.relation.CollectionTextRelation;
+import org.apache.ctakes.typesystem.type.syntax.BaseToken;
+import org.apache.ctakes.typesystem.type.syntax.ConllDependencyNode;
+import org.apache.ctakes.typesystem.type.textsem.*;
+import org.apache.ctakes.typesystem.type.textspan.ListEntry;
+import org.apache.ctakes.typesystem.type.textspan.Paragraph;
+import org.apache.ctakes.typesystem.type.textspan.Segment;
+import org.apache.ctakes.typesystem.type.textspan.Sentence;
+import org.apache.log4j.Logger;
+import org.apache.uima.fit.util.FSCollectionFactory;
+import org.apache.uima.fit.util.JCasUtil;
+import org.apache.uima.jcas.JCas;
+import org.apache.uima.jcas.cas.FSList;
+import org.apache.uima.jcas.tcas.Annotation;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import static org.apache.ctakes.core.pipeline.PipeBitInfo.TypeProduct.*;
+
+/**
+ * @author SPF , chip-nlp
+ * @version %I%
+ * @since 9/8/2016
+ */
+@PipeBitInfo(
+      name = "HTML Writer",
+      description = "Writes html files with document text and simple markups (Semantic Group, CUI, Negation).",
+      role = PipeBitInfo.Role.WRITER,
+      dependencies = { DOCUMENT_ID, SENTENCE, BASE_TOKEN },
+      usables = { DOCUMENT_ID_PREFIX, IDENTIFIED_ANNOTATION, EVENT, TIMEX, TEMPORAL_RELATION }
+)
+final public class HtmlTextWriter extends AbstractJCasFileWriter {
+
+   // TODO https://www.w3schools.com/howto/howto_css_switch.asp
+   // TODO https://www.w3schools.com/html/tryit.asp?filename=tryhtml_layout_flexbox
+   // TODO https://www.w3schools.com/html/html5_new_elements.asp
+
+// TODO https://css-tricks.com/snippets/css/a-guide-to-flexbox/
+// TODO https://www.quackit.com/css/flexbox/tutorial/nested_flex_containers.cfm
+
+   static final String TOOL_TIP = "TIP";
+
+   static final String UNCERTAIN_NEGATED = "UNN_";
+   static final String NEGATED = "NEG_";
+   static final String UNCERTAIN = "UNC_";
+   static final String AFFIRMED = "AFF_";
+   static final String GENERIC = "GNR_";
+   static final String SPACER = "SPC_";
+   static final String NEWLINE = "NL_";
+   static final String WIKI_BEGIN = "WIK_";
+   static final String WIKI_CENTER = "_WK_";
+   static final String WIKI_END = "_WIK";
+
+   static private final Logger LOGGER = Logger.getLogger( "HtmlTextWriter" );
+
+   static private final String PREFERRED_TERM_UNKNOWN = "Unknown Preferred Term";
+   static private final String CTAKES_VERSION = "4.0.1";
+
+   static private final String FILE_EXTENSION = ".pretty.html";
+   static private final String CSS_FILENAME = "ctakes.pretty.css";
+   static private final String JS_FILENAME = "ctakes.pretty.js";
+
+   static private final Collection<String> _usedDirectories = ConcurrentHashMap.newKeySet();
+
+   /**
+    * {@inheritDoc}
+    */
+   @Override
+   public void writeFile( final JCas jCas,
+                          final String outputDir,
+                          final String documentId,
+                          final String fileName ) throws IOException {
+      synchronized (_usedDirectories) {
+         if ( _usedDirectories.add( outputDir ) ) {
+            final String cssPath = outputDir + '/' + CSS_FILENAME;
+            CssWriter.writeCssFile( cssPath );
+            final String jsPath = outputDir + '/' + JS_FILENAME;
+            JsWriter.writeJsFile( jsPath );
+         }
+      }
+      final File htmlFile = new File( outputDir, fileName + FILE_EXTENSION );
+      LOGGER.info( "Writing HTML to " + htmlFile.getPath() + " ..." );
+      try ( final BufferedWriter writer = new BufferedWriter( new FileWriter( htmlFile ) ) ) {
+         final String title = DocumentIDAnnotationUtil.getDocumentID( jCas );
+         writer.write( startBody() );
+         writer.write( getCssLink( CSS_FILENAME ) );
+         writer.write( getJsLink( JS_FILENAME ) );
+         writer.write( startContainer() );
+         writer.write( getHeader( title ) );
+         writer.write( getNav() );
+         writer.write( startArticle() );
+
+         final Collection<Segment> sections = JCasUtil.select( jCas, Segment.class );
+         final Map<Segment, Collection<org.apache.ctakes.typesystem.type.textspan.List>> lists
+               = JCasUtil.indexCovered( jCas, Segment.class, org.apache.ctakes.typesystem.type.textspan.List.class );
+         final Map<org.apache.ctakes.typesystem.type.textspan.List, Collection<ListEntry>> listEntries
+               = JCasUtil.indexCovered( jCas, org.apache.ctakes.typesystem.type.textspan.List.class, ListEntry.class );
+         final Map<Segment, Collection<Sentence>> sectionSentences
+               = JCasUtil.indexCovered( jCas, Segment.class, Sentence.class );
+         final Map<Sentence, Collection<IdentifiedAnnotation>> sentenceAnnotations
+               = JCasUtil.indexCovered( jCas, Sentence.class, IdentifiedAnnotation.class );
+         final Map<Sentence, Collection<BaseToken>> sentenceTokens
+               = JCasUtil.indexCovered( jCas, Sentence.class, BaseToken.class );
+         final Collection<BinaryTextRelation> relations = JCasUtil.select( jCas, BinaryTextRelation.class );
+         // TODO at each paragraph end index add a newline unless it is the end of a section
+         final Collection<Paragraph> paragraphs = JCasUtil.select( jCas, Paragraph.class );
+         cullAnnotations( sentenceAnnotations.values() );
+
+         final Collection<CollectionTextRelation> corefRelations = JCasUtil.select( jCas,
+               CollectionTextRelation.class );
+         final Map<Markable, TextSpan> markableSpans = mapMarkableSpans( jCas, corefRelations );
+         final Map<TextSpan, Collection<Integer>> corefSpans = createCorefSpans( corefRelations, markableSpans );
+
+         writeSections( sections, paragraphs, lists, listEntries, sectionSentences, sentenceAnnotations, sentenceTokens,
+               relations, corefSpans, writer );
+         writer.write( endArticle() );
+
+         writer.write( getFooter() );
+         writer.write( endContainer() );
+
+         writer.write( startJavascript() );
+         if ( !corefRelations.isEmpty() ) {
+            writeCorefInfos( corefRelations, writer );
+         }
+         writer.write( endJavascript() );
+
+         writer.write( endBody() );
+      }
+      LOGGER.info( "Finished Writing" );
+   }
+
+   static private void cullAnnotations( final Collection<Collection<IdentifiedAnnotation>> sentenceAnnotations ) {
+      final java.util.function.Predicate<IdentifiedAnnotation> keep = a -> EventMention.class.isInstance( a )
+            || TimeMention.class.isInstance( a ) || EntityMention.class.isInstance( a );
+      final Collection<IdentifiedAnnotation> keepers = new HashSet<>();
+      for ( Collection<IdentifiedAnnotation> annotations : sentenceAnnotations ) {
+         annotations.stream()
+               .filter( keep )
+               .forEach( keepers::add );
+         annotations.retainAll( keepers );
+         keepers.clear();
+      }
+   }
+
+   /**
+    * @param corefRelations coreference chains
+    * @return a map of markable text span ends to chain numbers
+    */
+   static private Map<TextSpan, Collection<Integer>> createCorefSpans( final Collection<CollectionTextRelation> corefRelations,
+                                                                       final Map<Markable, TextSpan> markableSpans ) {
+      if ( corefRelations == null || corefRelations.isEmpty() ) {
+         return Collections.emptyMap();
+      }
+      final Map<TextSpan, Collection<Integer>> corefSpans = new HashMap<>();
+      int index = 1;
+      for ( CollectionTextRelation corefRelation : corefRelations ) {
+         final FSList chainHead = corefRelation.getMembers();
+         final Collection<Markable> markables = FSCollectionFactory.create( chainHead, Markable.class );
+         for ( Markable markable : markables ) {
+            final TextSpan span = markableSpans.get( markable );
+            corefSpans.putIfAbsent( span, new ArrayList<>() );
+            corefSpans.get( span )
+                  .add( index );
+         }
+         index++;
+      }
+      return corefSpans;
+   }
+
+   /**
+    * This is a bit messy, but necessary.
+    *
+    * @param jCas           -
+    * @param corefRelations -
+    * @return map of markable to identified annotation
+    */
+   static private Map<Markable, TextSpan> mapMarkableSpans( final JCas jCas,
+                                                            final Collection<CollectionTextRelation> corefRelations ) {
+      if ( corefRelations == null || corefRelations.isEmpty() ) {
+         return Collections.emptyMap();
+      }
+      final Map<Markable, Collection<ConllDependencyNode>> markableNodes = JCasUtil.indexCovered( jCas, Markable.class,
+            ConllDependencyNode.class );
+      final Map<ConllDependencyNode, Collection<IdentifiedAnnotation>> nodeAnnotations
+            = JCasUtil.indexCovering( jCas, ConllDependencyNode.class, IdentifiedAnnotation.class );
+      cullAnnotations( nodeAnnotations.values() );
+      final Map<Markable, TextSpan> spanMap = new HashMap<>();
+      for ( CollectionTextRelation coref : corefRelations ) {
+         final Collection<Markable> markables = JCasUtil.select( coref.getMembers(), Markable.class );
+         for ( Markable markable : markables ) {
+            final Collection<ConllDependencyNode> nodes = markableNodes.get( markable );
+            if ( nodes == null || nodes.isEmpty() ) {
+               continue;
+            }
+            final ConllDependencyNode headNode = getNominalHeadNode( new ArrayList<>( nodes ) );
+            final Collection<IdentifiedAnnotation> annotations = nodeAnnotations.get( headNode );
+            if ( annotations == null || annotations.isEmpty() ) {
+               spanMap.put( markable, new DefaultTextSpan( headNode.getBegin(), headNode.getEnd() ) );
+               continue;
+            }
+            TextSpan bestSpan = null;
+            int bestLength = 0;
+            for ( IdentifiedAnnotation annotation : annotations ) {
+               if ( !EventMention.class.equals( annotation.getClass() )
+                     && annotation.getBegin() == markable.getBegin() && annotation.getEnd() == markable.getEnd() ) {
+                  // Prefer an exact non-event match over the longest match
+                  bestSpan = new DefaultTextSpan( annotation.getBegin(), annotation.getEnd() );
+                  break;
+               }
+               if ( annotation.getEnd() - annotation.getBegin() > bestLength ) {
+                  bestLength = annotation.getEnd() - annotation.getBegin();
+                  bestSpan = new DefaultTextSpan( annotation.getBegin(), annotation.getEnd() );
+               }
+            }
+            if ( bestSpan != null ) {
+               spanMap.put( markable, bestSpan );
+            } else {
+               spanMap.put( markable, new DefaultTextSpan( headNode.getBegin(), headNode.getEnd() ) );
+            }
+         }
+      }
+      return spanMap;
+   }
+
+   /**
+    * Finds the head node out of a few ConllDependencyNodes. Biased toward nouns.
+    **/
+   public static ConllDependencyNode getNominalHeadNode(
+         List<ConllDependencyNode> nodes ) {
+      ArrayList<ConllDependencyNode> anodes = new ArrayList<>( nodes );
+      Boolean[][] matrixofheads = new Boolean[ anodes.size() ][ anodes.size() ];
+      List<ConllDependencyNode> outnodes = new ArrayList<>();
+
+      // Remove root from consideration
+      for ( int i = 0; i < anodes.size(); i++ ) {
+         if ( anodes.get( i )
+               .getId() == 0 ) {
+            anodes.remove( i );
+         }
+      }
+
+      // Create a dependency matrix
+      for ( int id1 = 0; id1 < anodes.size(); id1++ ) {
+         for ( int id2 = 0; id2 < anodes.size(); id2++ ) {
+            // no head-dependency relationship between id1 and id2
+            if ( id1 == id2 || anodes.get( id1 )
+                  .getId() != anodes.get( id2 )
+                  .getHead()
+                  .getId() ) {
+               matrixofheads[ id2 ][ id1 ] = false;
+            }
+            // a match
+            else {
+               matrixofheads[ id2 ][ id1 ] = true;
+            }
+         }
+      }
+
+      // Search the dependency matrix for the head
+      for ( int idhd = 0; idhd < anodes.size(); idhd++ ) {
+         boolean occupiedCol = false;
+         for ( int row = 0; row < anodes.size(); row++ ) {
+            if ( matrixofheads[ row ][ idhd ] ) {
+               occupiedCol = true;
+            }
+         }
+         if ( occupiedCol ) {
+            boolean occupiedRow = false;
+            for ( int col = 0; col < anodes.size(); col++ ) {
+               if ( matrixofheads[ idhd ][ col ] ) {
+                  occupiedRow = true;
+               }
+            }
+            if ( !occupiedRow ) {
+               outnodes.add( anodes.get( idhd ) );
+            }
+         }
+      }
+
+      // Unheaded phrases
+      if ( outnodes.isEmpty() ) {
+         // pick a noun from the left, if there is one
+         for ( int i = 0; i < anodes.size(); i++ ) {
+            if ( Pattern.matches( "N..?", anodes.get( i )
+                  .getPostag() ) ) {
+               return anodes.get( i );
+            }
+         }
+         // default to picking the rightmost node
+         return anodes.get( anodes.size() - 1 );
+      }
+      // Headed phrases
+      else {
+         // pick a noun from the left, if there is one
+         for ( int i = 0; i < outnodes.size(); i++ ) {
+            if ( Pattern.matches( "N..?", outnodes.get( i )
+                  .getPostag() ) ) {
+               return outnodes.get( i );
+            }
+         }
+         // otherwise, pick the rightmost node with dependencies
+         return outnodes.get( outnodes.size() - 1 );
+      }
+   }
+
+   // The assumption is that any given span can only have one exact EventMention.
+   static private IdentifiedAnnotation getEvent( final Collection<IdentifiedAnnotation> annotations ) {
+      return annotations.stream()
+            .filter( a -> EventMention.class.equals( a.getClass() ) )
+            .findAny()
+            .orElse( null );
+   }
+
+   /**
+    * @param annotationMap -
+    * @return map of umls annotations to events
+    */
+   static private Map<IdentifiedAnnotation, IdentifiedAnnotation> getAnnotationEvents( final Map<TextSpan, Collection<IdentifiedAnnotation>> annotationMap ) {
+      final Map<IdentifiedAnnotation, IdentifiedAnnotation> annotationEvents = new HashMap<>();
+      final Map<TextSpan, IdentifiedAnnotation> unusedEvents = new HashMap<>();
+      for ( Map.Entry<TextSpan, Collection<IdentifiedAnnotation>> entry : annotationMap.entrySet() ) {
+         final Collection<IdentifiedAnnotation> annotations = entry.getValue();
+         final IdentifiedAnnotation event = getEvent( annotations );
+         if ( event != null ) {
+            if ( annotations.size() > 1 ) {
+               final int pre = annotationEvents.size();
+               annotations.stream()
+                     .filter( EventMention.class::isInstance )
+                     .filter( a -> !event.equals( a ) )
+                     .forEach( a -> annotationEvents.put( a, event ) );
+               if ( annotationEvents.size() > pre ) {
+                  annotations.remove( event );
+               } else {
+                  unusedEvents.put( entry.getKey(), event );
+               }
+            } else {
+               unusedEvents.put( entry.getKey(), event );
+            }
+         }
+      }
+      if ( unusedEvents.isEmpty() ) {
+         return annotationEvents;
+      }
+      final Map<TextSpan, IdentifiedAnnotation> usedEvents = new HashMap<>();
+      for ( Map.Entry<TextSpan, Collection<IdentifiedAnnotation>> entry : annotationMap.entrySet() ) {
+         final TextSpan span = entry.getKey();
+         TextSpan usedEventSpan = null;
+         for ( Map.Entry<TextSpan, IdentifiedAnnotation> unusedEvent : unusedEvents.entrySet() ) {
+            if ( !span.equals( unusedEvent.getKey() ) && span.contains( unusedEvent.getKey() ) ) {
+               entry.getValue()
+                     .stream()
+                     .filter( EventMention.class::isInstance )
+                     .forEach( a -> annotationEvents.put( a, unusedEvent.getValue() ) );
+               usedEventSpan = unusedEvent.getKey();
+               usedEvents.put( usedEventSpan, unusedEvent.getValue() );
+               break;
+            }
+         }
+         if ( usedEventSpan != null ) {
+            unusedEvents.remove( usedEventSpan );
+            if ( unusedEvents.isEmpty() ) {
+               break;
+            }
+         }
+      }
+      usedEvents.forEach( ( s, e ) -> annotationMap.get( s )
+            .remove( e ) );
+      final Collection<TextSpan> emptySpans = annotationMap.entrySet()
+            .stream()
+            .filter( e -> e.getValue()
+                  .isEmpty() )
+            .map( Map.Entry::getKey )
+            .collect( Collectors.toList() );
+      annotationMap.keySet()
+            .removeAll( emptySpans );
+      return annotationEvents;
+   }
+
+   /**
+    * @return html to start the body
+    */
+   static private String startBody() {
+      return "<!DOCTYPE html>\n" +
+            "<html>\n" +
+            "<body>\n";
+   }
+
+   /**
+    * @param filePath path to the css file
+    * @return html to link to css
+    */
+   static private String getCssLink( final String filePath ) {
+      return "<link rel=\"stylesheet\" href=\"" + filePath + "\" type=\"text/css\" media=\"screen\">";
+   }
+
+   /**
+    * @param filePath path to the js file
+    * @return html to link to js
+    */
+   static private String getJsLink( final String filePath ) {
+      return "<script type=\"text/javascript\" src=\"ctakes.pretty.js\"></script>\n";
+   }
+
+   static private String startJavascript() {
+      return "<script type=\"text/javascript\">";
+   }
+
+   static private String endJavascript() {
+      return "</script>";
+   }
+
+   static private String startContainer() {
+      return "<div class=\"flex-container\">\n";
+   }
+
+   static private String getHeader( final String title ) {
+      return "<header>\n" +
+            "  <h1>" + title + "</h1>\n" +
+            "</header>\n";
+   }
+
+   /**
+    * html for right-hand annotation information panel
+    */
+   static private String getNav() {
+      return "<nav class=\"nav\">\n" +
+            "    <div id=\"ia\">\n" +
+            "      Annotation Information\n" +
+            "    </div>\n" +
+            getLegend() +
+            "</nav>\n";
+   }
+
+   static private String startArticle() {
+      return "<article class=\"article\">\n";
+   }
+
+   static private String endArticle() {
+      return "</article>\n";
+   }
+
+   static private String getFooter() {
+      final LocalDateTime time = LocalDateTime.now();
+      final DateTimeFormatter formatter = DateTimeFormatter.ofPattern( "LL dd yyyy, HH:mm:ss" );
+      return "<footer>\n" +
+            "Processed by Apache cTAKES<sup>&copy;</sup> on " + formatter.format( time ) + "\n" +
+            "</footer>\n";
+   }
+
+   static private String endContainer() {
+      return "</div>\n";
+   }
+
+   /**
+    * write html for all sections (all text) in the document
+    *
+    * @param sectionSentences    map of sections and their contained sentences
+    * @param sentenceAnnotations map of sentences and their contained annotations
+    * @param sentenceTokens      map of sentences and their contained base tokens
+    * @param relations           all relations
+    * @param corefSpans          map of text spans to coreference chain indices
+    * @param writer              writer to which pretty html for the section should be written
+    * @throws IOException if the writer has issues
+    */
+   static private void writeSections( final Map<Segment, Collection<Sentence>> sectionSentences,
+                                      final Collection<Paragraph> paragraphs,
+                                      final Map<Sentence, Collection<IdentifiedAnnotation>> sentenceAnnotations,
+                                      final Map<Sentence, Collection<BaseToken>> sentenceTokens,
+                                      final Collection<BinaryTextRelation> relations,
+                                      final Map<TextSpan, Collection<Integer>> corefSpans,
+                                      final BufferedWriter writer ) throws IOException {
+      final Collection<Integer> paragraphBegins = paragraphs.stream()
+            .map( Annotation::getBegin )
+            .collect( Collectors.toList() );
+      final List<Segment> sections = new ArrayList<>( sectionSentences.keySet() );
+      sections.sort( Comparator.comparingInt( Segment::getBegin ) );
+      for ( Segment section : sections ) {
+         writeSectionHeader( section, writer );
+         writer.write( "\n<p>\n" );
+         final List<Sentence> sentences = new ArrayList<>( sectionSentences.get( section ) );
+         sentences.sort( Comparator.comparingInt( Sentence::getBegin ) );
+         for ( Sentence sentence : sentences ) {
+            final Collection<IdentifiedAnnotation> annotations = sentenceAnnotations.get( sentence );
+            final Collection<BaseToken> tokens = sentenceTokens.get( sentence );
+            writeSentence( sentence, annotations, tokens, relations, corefSpans, writer );
+            if ( paragraphBegins.contains( sentence.getEnd() ) ) {
+               writer.write( "\n</p>\n<p>\n" );
+            }
+         }
+         writer.write( "\n</p>\n" );
+      }
+   }
+
+   /**
+    * write html for all sections (all text) in the document
+    *
+    * @param sectionSentences    map of sections and their contained sentences
+    * @param sentenceAnnotations map of sentences and their contained annotations
+    * @param sentenceTokens      map of sentences and their contained base tokens
+    * @param relations           all relations
+    * @param corefSpans          map of text span ends to coreference chain indices
+    * @param writer              writer to which pretty html for the section should be written
+    * @throws IOException if the writer has issues
+    */
+   static private void writeSections( final Collection<Segment> sectionSet,
+                                      final Collection<Paragraph> paragraphs,
+                                      final Map<Segment, Collection<org.apache.ctakes.typesystem.type.textspan.List>> lists,
+                                      final Map<org.apache.ctakes.typesystem.type.textspan.List, Collection<ListEntry>> listEntries,
+                                      final Map<Segment, Collection<Sentence>> sectionSentences,
+                                      final Map<Sentence, Collection<IdentifiedAnnotation>> sentenceAnnotations,
+                                      final Map<Sentence, Collection<BaseToken>> sentenceTokens,
+                                      final Collection<BinaryTextRelation> relations,
+                                      final Map<TextSpan, Collection<Integer>> corefSpans,
+                                      final BufferedWriter writer ) throws IOException {
+      if ( lists.isEmpty() ) {
+         writeSections( sectionSentences, paragraphs, sentenceAnnotations, sentenceTokens, relations, corefSpans,
+               writer );
+         return;
+      }
+      final Collection<Integer> paragraphBegins = paragraphs.stream()
+            .map( Annotation::getBegin )
+            .collect( Collectors.toList() );
+      final List<Segment> sections = new ArrayList<>( sectionSet );
+      sections.sort( Comparator.comparingInt( Segment::getBegin ) );
+      final Map<Integer, Integer> enclosers = new HashMap<>();
+      for ( Map.Entry<org.apache.ctakes.typesystem.type.textspan.List, Collection<ListEntry>> entry : listEntries.entrySet() ) {
+         final int listEnd = entry.getKey()
+               .getEnd();
+         entry.getValue()
+               .forEach( e -> enclosers.put( e.getBegin(), listEnd ) );
+      }
+      for ( Segment section : sections ) {
+         writeSectionHeader( section, writer );
+         final Collection<Sentence> sentenceSet = sectionSentences.get( section );
+         if ( sentenceSet == null ) {
+            continue;
+         }
+         writer.write( "\n<p>\n" );
+         final List<Sentence> sentences = new ArrayList<>( sentenceSet );
+         sentences.sort( Comparator.comparingInt( Sentence::getBegin ) );
+         int currentEnd = -1;
+         boolean freshEntry = false;
+         for ( Sentence sentence : sentences ) {
+            final Collection<IdentifiedAnnotation> annotations = sentenceAnnotations.get( sentence );
+            final Collection<BaseToken> tokens = sentenceTokens.get( sentence );
+            final Integer end = enclosers.get( sentence.getBegin() );
+            if ( end != null ) {
+               freshEntry = true;
+               if ( currentEnd < 0 ) {
+                  startList( sentence, annotations, tokens, relations, corefSpans, writer );
+                  currentEnd = end;
+               } else {
+                  writeListEntry( sentence, annotations, tokens, relations, corefSpans, writer );
+               }
+            } else {
+               if ( currentEnd >= 0 && sentence.getBegin() > currentEnd ) {
+                  endList( sentence, annotations, tokens, relations, corefSpans, writer );
+                  currentEnd = -1;
+                  freshEntry = false;
+                  continue;
+               }
+               if ( freshEntry ) {
+                  freshEntry = false;
+                  writer.write( "\n<br>\n" );
+               }
+               writeSentence( sentence, annotations, tokens, relations, corefSpans, writer );
+               if ( paragraphBegins.contains( sentence.getEnd() ) ) {
+                  writer.write( "\n</p>\n<p>\n" );
+               }
+            }
+         }
+         if ( currentEnd >= 0 ) {
+            endList( writer );
+         }
+         writer.write( "\n</p>\n" );
+      }
+   }
+
+   /**
+    * @param sentence     sentence of interest
+    * @param annotations  identified annotations in the section
+    * @param baseTokenMap baseTokens in the section
+    * @param relations    all relations
+    * @param corefSpans   map of text span ends to coreference chain indices
+    * @return marked up text
+    */
+   static private String createLineText( final Sentence sentence,
+                                         final Collection<IdentifiedAnnotation> annotations,
+                                         final Map<TextSpan, String> baseTokenMap,
+                                         final Collection<BinaryTextRelation> relations,
+                                         final Map<TextSpan, Collection<Integer>> corefSpans ) {
+      final Map<TextSpan, Collection<IdentifiedAnnotation>> annotationMap = createAnnotationMap( sentence,
+            annotations );
+      final Map<IdentifiedAnnotation, IdentifiedAnnotation> annotationEvents = getAnnotationEvents( annotationMap );
+      final Map<Integer, String> tags = createTags( sentence, annotationMap, annotationEvents, relations, corefSpans );
+      final StringBuilder sb = new StringBuilder();
+      int previousIndex = -1;
+      for ( Map.Entry<TextSpan, String> entry : baseTokenMap.entrySet() ) {
+         final String text = entry.getValue();
+         final int begin = entry.getKey()
+               .getBegin();
+         if ( begin != previousIndex ) {
+            final String beginTag = tags.get( begin );
+            if ( beginTag != null ) {
+               sb.append( beginTag );
+            }
+         }
+         sb.append( text );
+         final int end = entry.getKey()
+               .getEnd();
+         final String endTag = tags.get( end );
+         if ( endTag != null ) {
+            sb.append( endTag );
+         }
+         sb.append( " " );
+         previousIndex = end;
+      }
+      return sb.toString();
+   }
+
+   static private void startList( final Sentence sentence,
+                                  final Collection<IdentifiedAnnotation> annotations,
+                                  final Collection<BaseToken> baseTokens,
+                                  final Collection<BinaryTextRelation> relations,
+                                  final Map<TextSpan, Collection<Integer>> corefSpans,
+                                  final BufferedWriter writer ) throws IOException {
+      if ( baseTokens.isEmpty() ) {
+         return;
+      }
+      // Because of character substitutions, baseTokens and IdentifiedAnnotations have to be tied by text span
+      final Map<TextSpan, String> baseTokenMap = createBaseTokenMap( sentence, baseTokens );
+      if ( baseTokenMap.isEmpty() ) {
+         return;
+      }
+      writer.write( "\n<ul>\n<li>" );
+      final String lineText = createLineText( sentence, annotations, baseTokenMap, relations, corefSpans );
+      writer.write( lineText );
+   }
+
+   /**
+    * Write html for a sentence from the document text
+    *
+    * @param sentence    sentence of interest
+    * @param annotations identified annotations in the section
+    * @param baseTokens  baseTokens in the section
+    * @param relations   all relations
+    * @param writer      writer to which pretty html for the section should be written
+    * @throws IOException if the writer has issues
+    */
+   static private void writeListEntry( final Sentence sentence,
+                                       final Collection<IdentifiedAnnotation> annotations,
+                                       final Collection<BaseToken> baseTokens,
+                                       final Collection<BinaryTextRelation> relations,
+                                       final Map<TextSpan, Collection<Integer>> corefSpans,
+                                       final BufferedWriter writer ) throws IOException {
+      if ( baseTokens.isEmpty() ) {
+         return;
+      }
+      // Because of character substitutions, baseTokens and IdentifiedAnnotations have to be tied by text span
+      final Map<TextSpan, String> baseTokenMap = createBaseTokenMap( sentence, baseTokens );
+      if ( baseTokenMap.isEmpty() ) {
+         return;
+      }
+      writer.write( "</li>\n<li>" );
+      final String lineText = createLineText( sentence, annotations, baseTokenMap, relations, corefSpans );
+      writer.write( lineText );
+   }
+
+   static private void endList( final Sentence sentence,
+                                final Collection<IdentifiedAnnotation> annotations,
+                                final Collection<BaseToken> baseTokens,
+                                final Collection<BinaryTextRelation> relations,
+                                final Map<TextSpan, Collection<Integer>> corefSpans,
+                                final BufferedWriter writer ) throws IOException {
+      if ( baseTokens.isEmpty() ) {
+         return;
+      }
+      // Because of character substitutions, baseTokens and IdentifiedAnnotations have to be tied by text span
+      final Map<TextSpan, String> baseTokenMap = createBaseTokenMap( sentence, baseTokens );
+      if ( baseTokenMap.isEmpty() ) {
+         return;
+      }
+      final String lineText = createLineText( sentence, annotations, baseTokenMap, relations, corefSpans );
+      writer.write( lineText + "</li>\n</ul>\n" );
+   }
+
+   static private void endList( final BufferedWriter writer ) throws IOException {
+      writer.write( "</li>\n</ul>\n" );
+   }
+
+   /**
+    * write html for section header
+    *
+    * @param section -
+    * @param writer  writer to which pretty html for the section should be written
+    * @throws IOException if the writer has issues
+    */
+   static private void writeSectionHeader( final Segment section, final BufferedWriter writer ) throws IOException {
+      String sectionId = section.getId();
+      if ( sectionId.equals( "SIMPLE_SEGMENT" ) ) {
+         return;
+      }
+      final StringBuilder sb = new StringBuilder();
+      sb.append( "\n<h3" );
+      final String sectionTag = getSafeText( section.getTagText() );
+      if ( sectionTag != null && !sectionTag.trim()
+            .isEmpty() ) {
+         sb.append( " onClick=\"iaf(\'" )
+               .append( sectionTag.trim() )
+               .append( "')\"" );
+      }
+      sb.append( ">" )
+            .append( getSafeText( sectionId ) );
+      final String sectionName = section.getPreferredText();
+      if ( sectionName != null && !sectionName.trim()
+            .isEmpty() && !sectionName.trim()
+            .equals( sectionId ) ) {
+         sb.append( " : " )
+               .append( getSafeText( sectionName ) );
+      }
+      sb.append( "</h3>\n" );
+      writer.write( sb.toString() );
+   }
+
+
+   /**
+    * Write html for a sentence from the document text
+    *
+    * @param sentence    sentence of interest
+    * @param annotations identified annotations in the section
+    * @param baseTokens  baseTokens in the section
+    * @param relations   all relations
+    * @param corefSpans  map of text span ends to coreference chain indices
+    * @param writer      writer to which pretty html for the section should be written
+    * @throws IOException if the writer has issues
+    */
+   static private void writeSentence( final Sentence sentence,
+                                      final Collection<IdentifiedAnnotation> annotations,
+                                      final Collection<BaseToken> baseTokens,
+                                      final Collection<BinaryTextRelation> relations,
+                                      final Map<TextSpan, Collection<Integer>> corefSpans,
+                                      final BufferedWriter writer ) throws IOException {
+      if ( baseTokens.isEmpty() ) {
+         return;
+      }
+      // Because of character substitutions, baseTokens and IdentifiedAnnotations have to be tied by text span
+      final Map<TextSpan, String> baseTokenMap = createBaseTokenMap( sentence, baseTokens );
+      if ( baseTokenMap.isEmpty() ) {
+         return;
+      }
+      final String lineText = createLineText( sentence, annotations, baseTokenMap, relations, corefSpans );
+      writer.write( lineText + "\n<br>\n" );
+   }
+
+   /**
+    * removes empty spans and replaces non-html compatible characters with their html ok equivalents
+    *
+    * @param sentence   -
+    * @param baseTokens in the sentence
+    * @return a map of text spans and their contained text
+    */
+   static private Map<TextSpan, String> createBaseTokenMap( final Sentence sentence,
+                                                            final Collection<BaseToken> baseTokens ) {
+      final int sentenceBegin = sentence.getBegin();
+      final Map<TextSpan, String> baseItemMap = new LinkedHashMap<>();
+      for ( BaseToken baseToken : baseTokens ) {
+         final TextSpan textSpan = new DefaultTextSpan( baseToken, sentenceBegin );
+         if ( textSpan.getWidth() == 0 ) {
+            continue;
+         }
+         String text = getSafeText( baseToken );
+         if ( text.isEmpty() ) {
+            continue;
+         }
+         baseItemMap.put( textSpan, text );
+      }
+      return baseItemMap;
+   }
+
+   static private String getSafeText( final Annotation annotation ) {
+      if ( annotation == null ) {
+         return "";
+      }
+      return getSafeText( annotation.getCoveredText()
+            .trim() );
+   }
+
+   static private String getSafeText( final String text ) {
+      if ( text.isEmpty() ) {
+         return "";
+      }
+      String safeText = text.replaceAll( "'", "&apos;" );
+      safeText = safeText.replaceAll( "\"", "&quot;" );
+      safeText = safeText.replaceAll( "@", "&amp;" );
+      safeText = safeText.replaceAll( "<", "&lt;" );
+      safeText = safeText.replaceAll( ">", "&gt;" );
+      return safeText;
+   }
+
+   /**
+    * @param sentence    -
+    * @param annotations annotations within the sentence
+    * @return map of text spans and all annotations within those spans.  Accounts for overlap, etc.
+    */
+   static private Map<TextSpan, Collection<IdentifiedAnnotation>> createAnnotationMap( final Sentence sentence,
+                                                                                       final Collection<IdentifiedAnnotation> annotations ) {
+      final Map<TextSpan, Collection<IdentifiedAnnotation>> annotationMap = new HashMap<>();
+      final int sentenceBegin = sentence.getBegin();
+      for ( IdentifiedAnnotation annotation : annotations ) {
+         final TextSpan textSpan = new DefaultTextSpan( annotation, sentenceBegin );
+         if ( textSpan.getWidth() == 0 ) {
+            continue;
+         }
+         final Collection<SemanticGroup> semanticGroups = SemanticGroup.getGroups( annotation );
+         if ( !semanticGroups.isEmpty() ) {
+            annotationMap.putIfAbsent( textSpan, new ArrayList<>() );
+            annotationMap.get( textSpan )
+                  .add( annotation );
+         }
+      }
+      return annotationMap;
+   }
+
+   /**
+    * sorts by begins, then by ends if begins are equal
+    */
+   static private class TextSpanComparator implements Comparator<TextSpan> {
+      public int compare( final TextSpan t1, final TextSpan t2 ) {
+         int r = t1.getBegin() - t2.getBegin();
+         if ( r != 0 ) {
+            return r;
+         }
+         return t1.getEnd() - t2.getEnd();
+      }
+   }
+
+   static private final Comparator<TextSpan> TEXT_SPAN_COMPARATOR = new TextSpanComparator();
+
+   /**
+    * Creates map of text span indices and whether each span represents the beginning of one or more annotations,
+    * the inside of two or more overlapping annotations, or the end of two or more overlapping annotations
+    *
+    * @param textSpans -
+    * @return B I E map
+    */
+   static private Map<Integer, Character> createIndexMap( final Collection<TextSpan> textSpans ) {
+      if ( textSpans.isEmpty() ) {
+         return Collections.emptyMap();
+      }
+      final List<TextSpan> spanList = new ArrayList<>( textSpans );
+      spanList.sort( TEXT_SPAN_COMPARATOR );
+      final int spanCount = spanList.size();
+      final int spanCountMinus = spanCount - 1;
+      final Map<Integer, Character> indexMap = new HashMap<>();
+      for ( int i = 0; i < spanCountMinus; i++ ) {
+         final TextSpan textSpan = spanList.get( i );
+         final int begin = textSpan.getBegin();
+         indexMap.putIfAbsent( begin, 'B' );
+         final int end = textSpan.getEnd();
+         indexMap.putIfAbsent( end, 'E' );
+         for ( int j = i + 1; j < spanCount; j++ ) {
+            TextSpan nextSpan = spanList.get( j );
+            if ( nextSpan.getBegin() > end ) {
+               break;
+            }
+            if ( nextSpan.getBegin() > begin ) {
+               indexMap.put( nextSpan.getBegin(), 'I' );
+            }
+            if ( nextSpan.getEnd() < end ) {
+               indexMap.put( nextSpan.getEnd(), 'I' );
+            } else if ( nextSpan.getEnd() > end ) {
+               indexMap.put( end, 'I' );
+            }
+         }
+      }
+      final TextSpan lastSpan = spanList.get( spanCountMinus );
+      indexMap.putIfAbsent( lastSpan.getBegin(), 'B' );
+      indexMap.putIfAbsent( lastSpan.getEnd(), 'E' );
+      return indexMap;
+   }
+
+   /**
+    * @param indexMap map of text span indices and the B I E status of the spans
+    * @return new spans representing the smallest required unique span elements of overlapping spans
+    */
+   static private Collection<TextSpan> createAdjustedSpans( final Map<Integer, Character> indexMap ) {
+      if ( indexMap.isEmpty() ) {
+         return Collections.emptyList();
+      }
+      final List<Integer> indexList = new ArrayList<>( indexMap.keySet() );
+      Collections.sort( indexList );
+      final int indexCount = indexList.size();
+      final Collection<TextSpan> newSpans = new ArrayList<>();
+      Integer index1 = indexList.get( 0 );
+      Character c1 = indexMap.get( index1 );
+      for ( int i = 1; i < indexCount; i++ ) {
+         final Integer index2 = indexList.get( i );
+         final Character c2 = indexMap.get( index2 );
+         if ( c1.equals( 'B' ) || c1.equals( 'I' ) ) {
+            newSpans.add( new DefaultTextSpan( index1, index2 ) );
+         }
+         index1 = index2;
+         c1 = c2;
+      }
+      return newSpans;
+   }
+
+   /**
+    * @param adjustedList  spans representing the smallest required unique span elements of overlapping spans
+    * @param annotationMap map of larger overlapping text spans and their annotations
+    * @return map of all annotations within or overlapping the small span elements
+    */
+   static private Map<TextSpan, Collection<IdentifiedAnnotation>> createAdjustedAnnotations(
+         final List<TextSpan> adjustedList, final Map<TextSpan, Collection<IdentifiedAnnotation>> annotationMap ) {
+      final List<TextSpan> spanList = new ArrayList<>( annotationMap.keySet() );
+      spanList.sort( TEXT_SPAN_COMPARATOR );
+      final Map<TextSpan, Collection<IdentifiedAnnotation>> spanAnnotations = new HashMap<>( adjustedList.size() );
+      final int spanCount = spanList.size();
+      int previousMatchIndex = 0;
+      for ( TextSpan adjusted : adjustedList ) {
+         boolean matched = false;
+         for ( int i = previousMatchIndex; i < spanCount; i++ ) {
+            final TextSpan annotationsSpan = spanList.get( i );
+            if ( annotationsSpan.overlaps( adjusted ) ) {
+               if ( !matched ) {
+                  previousMatchIndex = i;
+                  matched = true;
+               }
+               spanAnnotations.putIfAbsent( adjusted, new HashSet<>() );
+               spanAnnotations.get( adjusted )
+                     .addAll( annotationMap.get( annotationsSpan ) );
+            }
+         }
+      }
+      return spanAnnotations;
+   }
+
+   static private Map<TextSpan, Collection<Integer>> getSentenceCorefs( final Sentence sentence,
+                                                                        final Map<TextSpan, Collection<Integer>> corefSpans ) {
+      final Map<TextSpan, Collection<Integer>> sentenceCorefs = new HashMap<>();
+      final int sentenceBegin = sentence.getBegin();
+      final int sentenceEnd = sentence.getEnd();
+      for ( Map.Entry<TextSpan, Collection<Integer>> entry : corefSpans.entrySet() ) {
+         final int entryBegin = entry.getKey()
+               .getBegin();
+         if ( entryBegin >= sentenceBegin && entryBegin < sentenceEnd ) {
+            sentenceCorefs.put(
+                  new DefaultTextSpan( entryBegin - sentenceBegin, entry.getKey()
+                        .getEnd() - sentenceBegin ),
+                  entry.getValue() );
+         }
+      }
+      return sentenceCorefs;
+   }
+
+   /**
+    * @param sentence      begin offset of sentence
+    * @param annotationMap map of all annotations within or overlapping the small span elements
+    * @param relations     all relations
+    * @param corefSpans    map of text span ends to coreference chain indices
+    * @return html for span elements
+    */
+   static private Map<Integer, String> createTags( final Sentence sentence,
+                                                   final Map<TextSpan, Collection<IdentifiedAnnotation>> annotationMap,
+                                                   final Map<IdentifiedAnnotation, IdentifiedAnnotation> annotationEvents,
+                                                   final Collection<BinaryTextRelation> relations,
+                                                   final Map<TextSpan, Collection<Integer>> corefSpans
+   ) {
+      if ( annotationMap.isEmpty() ) {
+         return Collections.emptyMap();
+      }
+      final Collection<TextSpan> spans = new HashSet<>( annotationMap.keySet() );
+      // TODO move coref adjustment uphill
+      final Map<TextSpan, Collection<Integer>> sentenceCorefs = getSentenceCorefs( sentence, corefSpans );
+      spans.addAll( sentenceCorefs.keySet() );
+      final Map<Integer, Character> indexMap = createIndexMap( spans );
+      final Collection<TextSpan> adjustedSpans = createAdjustedSpans( indexMap );
+      final List<TextSpan> adjustedList = new ArrayList<>( adjustedSpans );
+      adjustedList.sort( TEXT_SPAN_COMPARATOR );
+      final Map<TextSpan, Collection<IdentifiedAnnotation>> adjustedAnnotations
+            = createAdjustedAnnotations( adjustedList, annotationMap );
+
+      final int sentenceBegin = sentence.getBegin();
+      final Map<Integer, String> indexTags = new HashMap<>();
+      for ( TextSpan adjustedSpan : adjustedList ) {
+         final StringBuilder sb = new StringBuilder( "<span" );
+         final Collection<IdentifiedAnnotation> annotations = adjustedAnnotations.get( adjustedSpan );
+         final String polarityClasses = createPolaritiesText( annotations );
+         if ( !polarityClasses.isEmpty() ) {
+            sb.append( " class=\"" )
+                  .append( polarityClasses )
+                  .append( '\"' );
+         }
+         final String clickInfo = createClickInfo( annotations, annotationEvents, relations );
+         if ( !clickInfo.isEmpty() ) {
+            sb.append( " onClick=\"iaf(\'" )
+                  .append( clickInfo )
+                  .append( "\')\"" );
+         }
+         final String tip = createTipText( annotations );
+         if ( !tip.isEmpty() ) {
+            sb.append( " " + TOOL_TIP + "=\"" )
+                  .append( tip )
+                  .append( '\"' );
+         }
+         sb.append( '>' );
+         // coref chain
+         final int adjustedEnd = sentenceBegin + adjustedSpan.getEnd();
+         final StringBuilder sb2 = new StringBuilder();
+         final Collection<IdentifiedAnnotation> endAnnotations = getEndAnnotations( annotations, adjustedEnd );
+         final Collection<SemanticGroup> groups = endAnnotations.stream()
+               .map( SemanticGroup::getGroups )
+               .flatMap( Collection::stream )
+               .distinct()
+               .collect( Collectors.toList() );
+         final Collection<String> encodings = groups.stream()
+               .map( SemanticMarkup::getMarkup )
+               .map( SemanticMarkup::getEncoding )
+               .sorted()
+               .collect( Collectors.toList() );
+         String firstEncoding = encodings.stream()
+               .findAny()
+               .orElse( SemanticMarkup.UNKNOWN_MARK.getEncoding() );
+         if ( annotations != null && endAnnotations.size() != annotations.size() ) {
+            firstEncoding += " " + polarityClasses;
+         }
+         final Collection<Integer> chains = sentenceCorefs.get( adjustedSpan );
+         if ( chains != null && !chains.isEmpty() ) {
+            for ( Integer chain : chains ) {
+//               sb2.append( "<span class=\"" ).append( semantic ).append( "\"" );
+               sb2.append( "<span class=\"" )
+                     .append( firstEncoding )
+                     .append( "\"" );
+               sb2.append( " onClick=\"crf" )
+                     .append( chain )
+                     .append( "()\">" );
+               sb2.append( "<sup>" )
+                     .append( chain )
+                     .append( "</sup></span>" );
+            }
+         } else {
+            for ( SemanticGroup group : groups ) {
+               if ( group == SemanticGroup.EVENT
+                     || group == SemanticGroup.TIME
+                     || group == SemanticGroup.ENTITY
+                     || group == SemanticGroup.UNKNOWN ) {
+                  continue;
+               }
+               final SemanticMarkup markup = SemanticMarkup.getMarkup( group );
+               sb2.append( "<span class=\"" )
+                     .append( markup.getEncoding() );
+               if ( endAnnotations.size() != annotations.size() ) {
+                  sb2.append( " " )
+                        .append( polarityClasses );
+               }
+               sb2.append( "\"></span>" );
+            }
+         }
+
+         final Integer begin = adjustedSpan.getBegin();
+         final String previousTag = indexTags.getOrDefault( begin, "" );
+         indexTags.put( begin, previousTag + sb.toString() );
+         indexTags.put( adjustedSpan.getEnd(), "</span>" + sb2.toString() );
+      }
+      return indexTags;
+   }
+
+   static private Collection<IdentifiedAnnotation> getEndAnnotations( final Collection<IdentifiedAnnotation> annotations,
+                                                                      final int adjustedEnd ) {
+      if ( annotations == null || annotations.isEmpty() ) {
+         return Collections.emptyList();
+      }
+      return annotations.stream()
+            .filter( a -> a.getEnd() == adjustedEnd )
+            .collect( Collectors.toSet() );
+   }
+
+   /**
+    * @param annotations -
+    * @return html with annotation information: polarity, semantic, cui, text, pref text
+    */
+   static private String createClickInfo( final Collection<IdentifiedAnnotation> annotations,
+                                          final Map<IdentifiedAnnotation, IdentifiedAnnotation> annotationEvents,
+                                          final Collection<BinaryTextRelation> relations ) {
+      if ( annotations == null || annotations.isEmpty() ) {
+         return "";
+      }
+      final Map<String, Map<String, Collection<String>>> polarInfoMap = new HashMap<>();
+      for ( IdentifiedAnnotation annotation : annotations ) {
+         final String polarity = createPolarity( annotation );
+         polarInfoMap.putIfAbsent( polarity, new HashMap<>() );
+         final IdentifiedAnnotation event = annotationEvents.get( annotation );
+         final Map<String, Collection<String>> infoMap = createInfoMap( annotation, event, relations );
+         for ( Map.Entry<String, Collection<String>> infoEntry : infoMap.entrySet() ) {
+            polarInfoMap.get( polarity )
+                  .putIfAbsent( infoEntry.getKey(), new HashSet<>() );
+            polarInfoMap.get( polarity )
+                  .get( infoEntry.getKey() )
+                  .addAll( infoEntry.getValue() );
+         }
+      }
+      final List<String> polarities = new ArrayList<>( polarInfoMap.keySet() );
+      Collections.sort( polarities );
+      final StringBuilder sb = new StringBuilder();
+      for ( String polarity : polarities ) {
+         sb.append( polarity )
+               .append( NEWLINE );
+         final Map<String, Collection<String>> infoMap = polarInfoMap.get( polarity );
+         final List<String> semantics = new ArrayList<>( infoMap.keySet() );
+         Collections.sort( semantics );
+         for ( String semantic : semantics ) {
+            sb.append( semantic )
+                  .append( NEWLINE );
+            final List<String> texts = new ArrayList<>( infoMap.get( semantic ) );
+            Collections.sort( texts );
+            for ( String text : texts ) {
+               sb.append( text )
+                     .append( NEWLINE );
+            }
+         }
+      }
+      return sb.toString();
+   }
+
+   /**
+    * @param annotation -
+    * @return map of semantic to text for annotations
+    */
+   static private Map<String, Collection<String>> createInfoMap( final IdentifiedAnnotation annotation,
+                                                                 final IdentifiedAnnotation event,
+                                                                 final Collection<BinaryTextRelation> relations ) {
+      final Collection<UmlsConcept> concepts = OntologyConceptUtil.getUmlsConcepts( annotation );
+      final Map<String, Collection<String>> semanticMap = new HashMap<>();
+      final String coveredText = getCoveredText( annotation );
+      final String safeText = getSafeText( coveredText );
+      String relationText = getRelationText( annotation, relations );
+      if ( event != null ) {
+         relationText += getRelationText( event, relations );
+      }
+      for ( UmlsConcept concept : concepts ) {
+         final SemanticGroup group = SemanticTui.getTui( concept )
+               .getGroup();
+         final String encoding = SemanticMarkup.getMarkup( group )
+               .getEncoding();
+         semanticMap.putIfAbsent( encoding, new HashSet<>() );
+         final String prefText = getPreferredText( coveredText, concept );
+         String text = getWikiText( safeText, prefText ) + NEWLINE + getCodes( concept ) + getCodedPrefText(
+               prefText ) + relationText;
+         if ( annotation instanceof EventMention ) {
+            text += getDocTimeRel( (EventMention) annotation );
+         }
+         semanticMap.get( encoding )
+               .add( text );
+      }
+      if ( concepts.isEmpty() ) {
+         String postText = "";
+         final SemanticGroup group = SemanticGroup.getBestGroup( annotation );
+         final String encoding = SemanticMarkup.getMarkup( group )
+               .getEncoding();
+         if ( annotation instanceof EventMention ) {
+            postText = getDocTimeRel( (EventMention) annotation );
+         }
+         semanticMap.putIfAbsent( encoding, new HashSet<>() );
+         semanticMap.get( encoding )
+               .add( safeText + NEWLINE + postText + relationText );
+      }
+      return semanticMap;
+   }
+
+   static private String createWikiLink( final String coveredText, final String wikiText ) {
+      return WIKI_BEGIN + wikiText + WIKI_CENTER + coveredText + WIKI_END;
+   }
+
+   /**
+    * @param concept -
+    * @return cui if it exists and any codes if they exist
+    */
+   static private String getCodes( final UmlsConcept concept ) {
+      String codes = "";
+      final String cui = concept.getCui();
+      if ( cui != null && !cui.isEmpty() ) {
+         codes += SPACER + cui + NEWLINE;
+      }
+      final String tui = concept.getTui();
+      if ( tui != null && !tui.isEmpty() ) {
+         codes += SPACER + tui + NEWLINE;
+      }
+      final String code = concept.getCode();
+      if ( code != null && !code.isEmpty() ) {
+         codes += SPACER + code + NEWLINE;
+      }
+      return codes;
+   }
+
+   /**
+    * @param annotation -
+    * @return the covered text
+    */
+   static private String getCoveredText( final IdentifiedAnnotation annotation ) {
+      return annotation.getCoveredText()
+            .replace( '\r', ' ' )
+            .replace( '\n', ' ' );
+   }
+
+   /**
+    * @param coveredText -
+    * @param concept     -
+    * @return the covered text plus preferred text if it exists and is not equal to the covered text
+    */
+   static private String getPreferredText( final String coveredText, final UmlsConcept concept ) {
+      final String preferredText = concept.getPreferredText();
+      if ( preferredText != null && !preferredText.isEmpty()
+            && !preferredText.equals( PREFERRED_TERM_UNKNOWN )
+            && !preferredText.equalsIgnoreCase( coveredText )
+            && !preferredText.equalsIgnoreCase( coveredText + 's' )
+            && !coveredText.equalsIgnoreCase( preferredText + 's' ) ) {
+         return getSafeText( preferredText );
+      }
+      return "";
+   }
+
+   static private String getCodedPrefText( final String preferredText ) {
+      if ( !preferredText.isEmpty() ) {
+         return SPACER + "[" + preferredText + "]" + NEWLINE;
+      }
+      return "";
+   }
+
+   static private String getWikiText( final String coveredText, final String preferredText ) {
+      String wikiText = coveredText;
+      // oddly enough, searches more frequently have the covered text instead of the preferred text
+//      if ( preferredText != null && !preferredText.isEmpty() && !preferredText.contains( "," ) ) {
+//         wikiText = preferredText;
+//      }
+//      return WIKI_BEGIN + wikiText.replace( ' ', '_' ).toLowerCase() + WIKI_CENTER + coveredText + WIKI_END;  // wikipedia
+//      return WIKI_BEGIN + wikiText.replace( ' ', '%' ).toLowerCase() + WIKI_CENTER + coveredText + WIKI_END;  // webmd
+      return WIKI_BEGIN + wikiText.replace( ' ', '+' )
+            .toLowerCase() + WIKI_CENTER + coveredText + WIKI_END;  // most sites
+   }
+
+   /**
+    * @param eventMention -
+    * @return a line of text with doctimerel if available
+    */
+   static private String getDocTimeRel( final EventMention eventMention ) {
+      final Event event = eventMention.getEvent();
+      if ( event == null ) {
+         return "";
+      }
+      final EventProperties eventProperties = event.getProperties();
+      if ( eventProperties == null ) {
+         return "";
+      }
+      final String dtr = eventProperties.getDocTimeRel();
+      if ( dtr == null || dtr.isEmpty() ) {
+         return "";
+      }
+      return SPACER + "[" + dtr.toLowerCase() + "] doc time" + NEWLINE;
+   }
+
+   /**
+    * @param annotations -
+    * @return polarity representation for all provided annotations
+    */
+   static private String createPolaritiesText( final Collection<IdentifiedAnnotation> annotations ) {
+      if ( annotations == null || annotations.isEmpty() ) {
+         return GENERIC;
+      }
+      return annotations.stream()
+            .map( HtmlTextWriter::createPolarity )
+            .distinct()
+            .sorted()
+            .collect( Collectors.joining( " " ) );
+   }
+
+   /**
+    * @param annotation -
+    * @return polarity for a single annotation
+    */
+   static private String createPolarity( final IdentifiedAnnotation annotation ) {
+      if ( annotation instanceof TimeMention || annotation instanceof EntityMention ) {
+         return GENERIC;
+      }
+      if ( annotation.getPolarity() < 0 ) {
+         if ( annotation.getUncertainty() > 0 ) {
+            return UNCERTAIN_NEGATED;
+         } else {
+            return NEGATED;
+         }
+      } else if ( annotation.getUncertainty() > 0 ) {
+         return UNCERTAIN;
+      } else {
+         return AFFIRMED;
+      }
+   }
+
+   /**
+    * @param annotations -
+    * @return tooltip text with semantic names for given annotations
+    */
+   static private String createTipText( final Collection<IdentifiedAnnotation> annotations ) {
+      if ( annotations == null || annotations.isEmpty() ) {
+         return "";
+      }
+      final Map<String, Integer> semanticCounts = getSemanticCounts( annotations );
+      final List<String> semantics = new ArrayList<>( semanticCounts.keySet() );
+      Collections.sort( semantics );
+      final StringBuilder sb = new StringBuilder();
+      for ( String semanticName : semantics ) {
+         sb.append( semanticName );
+         final int count = semanticCounts.get( semanticName );
+         if ( count > 1 ) {
+            sb.append( '(' )
+                  .append( count )
+                  .append( ')' );
+         }
+         sb.append( ' ' );
+      }
+      return sb.toString();
+   }
+
+   static private String getRelationText( final IdentifiedAnnotation annotation,
+                                          final Collection<BinaryTextRelation> relations ) {
+      return relations.stream()
+            .map( r -> getRelationText( annotation, r ) )
+            .collect( Collectors.joining() );
+   }
+
+   static private String getRelationText( final IdentifiedAnnotation annotation,
+                                          final BinaryTextRelation relation ) {
+      if ( relation.getArg1()
+            .getArgument()
+            .equals( annotation ) ) {
+         return SPACER + "[" + relation.getCategory() + "] " + getSafeText( relation.getArg2()
+               .getArgument() ) + NEWLINE;
+      } else if ( relation.getArg2()
+            .getArgument()
+            .equals( annotation ) ) {
+         return SPACER + getSafeText( relation.getArg1()
+               .getArgument() ) + " [" + relation.getCategory() + "]" + NEWLINE;
+      }
+      return "";
+   }
+
+   /**
+    * @param annotations -
+    * @return counts of semantic types for annotations
+    */
+   static private Map<String, Integer> getSemanticCounts( final Collection<IdentifiedAnnotation> annotations ) {
+      // Check concepts with the same cui can have multiple tuis.  This can make it look like there are extra counts.
+      final Collection<String> usedCuis = new HashSet<>();
+      final Map<String, Integer> semanticCounts = new HashMap<>();
+      for ( IdentifiedAnnotation annotation : annotations ) {
+         final String annotationSemanticName = SemanticGroup.getBestGroup( annotation )
+               .getName();
+         final Collection<UmlsConcept> concepts = OntologyConceptUtil.getUmlsConcepts( annotation );
+         for ( UmlsConcept concept : concepts ) {
+            if ( !usedCuis.add( concept.getCui() ) ) {
+               continue;
+            }
+            String semanticName = SemanticTui.getTui( concept )
+                  .getGroupName();
+            if ( semanticName.equals( SemanticGroup.UNKNOWN.getName() ) ) {
+               semanticName = annotationSemanticName;
+            }
+            semanticCounts.putIfAbsent( semanticName, 0 );
+            final int count = semanticCounts.get( semanticName );
+            semanticCounts.put( semanticName, count + 1 );
+         }
+         usedCuis.clear();
+         if ( concepts.isEmpty() ) {
+            semanticCounts.putIfAbsent( annotationSemanticName, 0 );
+            final int count = semanticCounts.get( annotationSemanticName );
+            semanticCounts.put( annotationSemanticName, count + 1 );
+         }
+      }
+      return semanticCounts;
+   }
+
+   /**
+    * This method needs to be in this class so that it can properly link the coref chain numbers
+    *
+    * @param corefRelations -
+    * @param writer         writer to which pretty html for the section should be written
+    * @throws IOException if the writer has issues
+    */
+   static private void writeCorefInfos( final Collection<CollectionTextRelation> corefRelations, final BufferedWriter writer )
+         throws IOException {
+      if ( corefRelations == null || corefRelations.isEmpty() ) {
+         return;
+      }
+      int index = 1;
+      for ( CollectionTextRelation corefRelation : corefRelations ) {
+         final FSList chainHead = corefRelation.getMembers();
+         final Collection<IdentifiedAnnotation> markables
+               = FSCollectionFactory.create( chainHead, IdentifiedAnnotation.class );
+         final String text = markables.stream()
+               .sorted( Comparator.comparingInt( Annotation::getBegin ) )
+               .map( HtmlTextWriter::getSafeText )
+               .collect( Collectors.joining( "<br>" ) );
+         writer.write( "  function crf" + index + "() {\n" );
+         writer.write(
+               "    document.getElementById(\"ia\").innerHTML = \"<br><h3>Coreference Chain</h3>" + text + "\";\n" );
+         writer.write( "  }\n" );
+         index++;
+      }
+   }
+
+   static private String getLegend() {
+      return "<div class=\"legend\"><h3>Legend</h3>\n" +
+            "  <hr>\n" +
+            "  <table style=\"line-height: 120%\">\n" +
+            "    <tr>\n" +
+            "      <td><span class=\"" + AFFIRMED + "\">Affirmed Event</span></td>\n" +
+            "      <td><span class=\"" + NEGATED + "\">Negated Event</span></td>\n" +
+            "    </tr>\n" +
+            "    <tr>\n" +
+            "      <td><span class=\"" + UNCERTAIN + "\">Uncertain Event</span></td>\n" +
+            "      <td><span class=\"" + UNCERTAIN_NEGATED + "\">Uncertain Negated</span></td>\n" +
+            "    </tr>\n" +
+            "    <tr>\n" +
+            "      <td><span class=\"" + GENERIC + "\">Time or Generic</span></td>\n" +
+            "    </tr>\n" +
+            "  </table>\n" +
+            "  <hr>\n" +
+            "  <table>\n" +
+            "    <tr>\n" +
+            "      <td>Sign / Symptom<span class=\"" + SemanticMarkup.FINDING_MARK.getEncoding() + "\"></span></td>\n" +
+            "      <td>Procedure<span class=\"" + SemanticMarkup.PROCEDURE_MARK.getEncoding() + "\"></span></td>\n" +
+            "    </tr>\n" +
+            "    <tr>\n" +
+            "      <td>Disease / Disorder<span class=\"" + SemanticMarkup.DISORDER_MARK.getEncoding() + "\"></span></td>\n" +
+            "      <td>Medication<span class=\"" + SemanticMarkup.DRUG_MARK.getEncoding() + "\"></span></td>\n" +
+            "    </tr>\n" +
+            "    <tr>\n" +
+            "      <td>Anatomical Site<span class=\"" + SemanticMarkup.ANATOMY_MARK.getEncoding() + "\"></span></td>\n" +
+            "    </tr>\n" +
+            "  </table>\n" +
+            "  <hr>\n" +
+            "  <table>\n" +
+            "    <tr>\n" +
+            "      <td>Coreference Element<span class=\"" + SemanticMarkup.ENTITY_MARK.getEncoding() + "\"><sup>1</sup></span></td>\n" +
+            "    </tr>\n" +
+            "  </table>\n" +
+            "</div>\n";
+   }
+
+   /**
+    * @return html to end body
+    */
+   static private String endBody() {
+      return "</body>\n" +
+            "</html>\n";
+   }
+
+
+}

Added: ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/cc/html/JsWriter.java
URL: http://svn.apache.org/viewvc/ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/cc/html/JsWriter.java?rev=1825907&view=auto
==============================================================================
--- ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/cc/html/JsWriter.java (added)
+++ ctakes/trunk/ctakes-core/src/main/java/org/apache/ctakes/core/cc/html/JsWriter.java Mon Mar  5 15:09:58 2018
@@ -0,0 +1,95 @@
+package org.apache.ctakes.core.cc.html;
+
+
+import org.apache.log4j.Logger;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
+import static org.apache.ctakes.core.cc.html.HtmlTextWriter.*;
+
+/**
+ * @author SPF , chip-nlp
+ * @version %I%
+ * @since 10/15/2016
+ */
+final class JsWriter {
+
+   static private final Logger LOGGER = Logger.getLogger( "JsWriter" );
+
+
+   private JsWriter() {
+   }
+
+   /**
+    * @param filePath path to css file
+    */
+   static void writeJsFile( final String filePath ) {
+      final File outputFile = new File( filePath );
+      outputFile.getParentFile()
+            .mkdirs();
+      try ( final BufferedWriter writer = new BufferedWriter( new FileWriter( outputFile ) ) ) {
+         writer.write( getSwapInfoScript() );
+      } catch ( IOException ioE ) {
+         LOGGER.error( "Could not not write js file " + outputFile.getPath() );
+         LOGGER.error( ioE.getMessage() );
+      }
+   }
+
+   /**
+    * A javascript function is used to expand annotation tooltips into formatted html
+    *
+    * @return javascript
+    */
+   static private String getSwapInfoScript() {
+      String functionText = "  function iaf(txt) {\n" +
+            "    var aff=txt.replace( /" + AFFIRMED + "/g,\"<br><h3>Affirmed</h3>\" );\n" +
+            "    var neg=aff.replace( /" + NEGATED + "/g,\"<br><h3>Negated</h3>\" );\n" +
+            "    var unc=neg.replace( /" + UNCERTAIN + "/g,\"<br><h3>Uncertain</h3>\" );\n" +
+            "    var unn=unc.replace( /" + UNCERTAIN_NEGATED + "/g,\"<br><h3>Uncertain, Negated</h3>\" );\n" +
+            "    var gnr=unn.replace( /" + GENERIC + "/g,\"\" );\n" +
+
+            "    var wik1=gnr.replace( /" + WIKI_BEGIN
+            + "/g,\"<a href=\\\"https://vsearch.nlm.nih.gov/vivisimo/cgi-bin/query-meta?v%3Aproject=medlineplus&v%3Asources=medlineplus-bundle&query=\" );\n" +
+            "    var wik2=wik1.replace( /" + WIKI_CENTER + "/g,\"\\\" target=\\\"_blank\\\">\" );\n" +
+            "    var wik3=wik2.replace( /" + WIKI_END + "/g,\"</a>\" );\n";
+
+      String previousCode = "wik3";
+      for ( SemanticMarkup markup : SemanticMarkup.values() ) {
+         functionText += createSemanticReplacements( markup, previousCode );
+         previousCode = markup.getEncoding()
+               .toLowerCase();
+      }
+      functionText +=
+            "    var spc=_unk_.replace( /" + SPACER + "/g,\"&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\" );\n" +
+                  "    var prf1=spc.replace( /\\[/g,\"<i>\" );\n" +
+                  "    var prf2=prf1.replace( /\\]/g,\"</i>\" );\n" +
+                  "    var nl=prf2.replace( /" + NEWLINE + "/g,\"<br>\" );\n" +
+                  "    document.getElementById(\"ia\").innerHTML = nl;\n" +
+                  "  }\n";
+      return functionText;
+   }
+
+   static private String createSemanticReplacements( final SemanticMarkup markup, final String previousCode ) {
+      return "    var " + markup.getEncoding()
+            .toLowerCase()
+            + "=" + previousCode
+            + ".replace( /" + markup.getEncoding() + "/g,\"<b>" + markup.getGroup()
+            .getName() + "</b>\" );\n";
+   }
+
+   //       Available decent search engines:
+   // https://en.wikipedia.org/wiki/ct_scan
+   // http://www.merckmanuals.com/home/SearchResults?query=ct+scan
+   // https://www.omim.org/search/?search=ct+scan
+   // https://medical-dictionary.thefreedictionary.com/ct+scan
+   // https://vsearch.nlm.nih.gov/vivisimo/cgi-bin/query-meta?v%3Aproject=medlineplus&v%3Asources=medlineplus-bundle&query=ct+scan
+   // https://www.medicinenet.com/script/main/srchcont.asp?src=ct+scan
+   // https://www.webmd.com/search/search_results/default.aspx?query=ct%20scan
+   // https://vsearch.nlm.nih.gov/vivisimo/cgi-bin/query-meta?query=ct+scan&v%3Aproject=nlm-main-website
+   //       Make a list? https://www.w3schools.com/howto/howto_css_dropdown.asp
+
+
+}



Mime
View raw message