001package com.studentgui.apppages; 002 003import java.awt.BorderLayout; 004import java.awt.Font; 005import java.awt.GridBagConstraints; 006import java.awt.GridBagLayout; 007import java.awt.Insets; 008import java.awt.event.ActionEvent; 009import java.awt.event.KeyEvent; 010import java.sql.SQLException; 011import java.time.LocalDate; 012import java.util.LinkedHashMap; 013import java.util.Map; 014 015import javax.swing.JButton; 016import javax.swing.JLabel; 017import javax.swing.JPanel; 018import javax.swing.JScrollPane; 019import javax.swing.SwingUtilities; 020 021import org.slf4j.Logger; 022import org.slf4j.LoggerFactory; 023 024import com.studentgui.uicomp.PhaseScoreField; 025 026/** 027 * Cortical Visual Impairment (CVI) assessment page. 028 * 029 * <p>Provides a structured scoring interface for evaluating the 10 characteristic behaviors 030 * associated with Cortical Visual Impairment as defined in the Roman-Lanzi CVI Range assessment 031 * framework. Skills are organized into two functional clusters:</p> 032 * 033 * <ul> 034 * <li><b>Phase 1 (P1_1–P1_6): Primary CVI Characteristics</b> 035 * <ul> 036 * <li><b>Color Preference:</b> Preference for high-saturation colors (red, yellow)</li> 037 * <li><b>Need for Movement:</b> Improved visual attention with motion</li> 038 * <li><b>Latency:</b> Delayed visual response times</li> 039 * <li><b>Field Preference:</b> Asymmetric visual field usage patterns</li> 040 * <li><b>Visual Complexity:</b> Difficulty with cluttered/busy visual environments</li> 041 * <li><b>Nonpurposeful Gaze:</b> Reduced sustained visual fixation</li> 042 * </ul> 043 * </li> 044 * <li><b>Phase 2 (P2_1–P2_4): Secondary/Environmental Characteristics</b> 045 * <ul> 046 * <li><b>Distance Viewing:</b> Reduced effectiveness at distance</li> 047 * <li><b>Atypical Reflexes:</b> Blink-to-threat, light reflex variations</li> 048 * <li><b>Visual Novelty:</b> Preference for familiar objects/environments</li> 049 * <li><b>Visual Reach:</b> Difficulty localizing and reaching toward objects</li> 050 * </ul> 051 * </li> 052 * </ul> 053 * 054 * <p><b>Scoring and Interpretation:</b> Each characteristic is typically scored on a 0–10 scale 055 * representing frequency/severity of the behavior. Higher scores may indicate greater impact 056 * depending on the specific assessment protocol in use. Consult the Roman-Lanzi CVI Range manual 057 * for standardized scoring guidelines.</p> 058 * 059 * <p><b>Data Management:</b></p> 060 * <ul> 061 * <li>Scores captured via {@link PhaseScoreField} components with integer validation</li> 062 * <li>Submit button persists to database via {@link com.studentgui.apphelpers.Database#insertAssessmentResults}</li> 063 * <li>Session JSON exported to {@code StudentDataFiles/<student>/Sessions/CVI/CVI-<sessionId>-<timestamp>.json}</li> 064 * <li>Time-series plots generated per phase group and saved to {@code plots/} directory</li> 065 * <li>Markdown and HTML reports generated with embedded plots and color-coded legends</li> 066 * </ul> 067 * 068 * <p>The shared {@link JLineGraph} component visualizes trends across multiple sessions, 069 * grouped by phase to separate primary and secondary characteristics. This page does not 070 * implement listener interfaces as it operates on static student/date parameters.</p> 071 * 072 * @see com.studentgui.apphelpers.Database 073 * @see JLineGraph 074 * @see PhaseScoreField 075 */ 076public class CVI extends JPanel { 077 private static final Logger LOG = LoggerFactory.getLogger(CVI.class); 078 /** Mapping of assessment part codes to their input components. */ 079 private final Map<String, PhaseScoreField> inputs = new LinkedHashMap<>(); 080 081 /** Selected student display name (may be null) used when saving or plotting. */ 082 private final String studentNameParam; 083 084 /** Session date to associate with saved CVI progress entries. */ 085 private final LocalDate dateParam; 086 087 /** Shared graph component used to visualize recent CVI results. */ 088 private final JLineGraph graph; 089 090 /** 091 * Construct the CVI page bound to the selected student and session date. 092 * 093 * @param studentName selected student name (may be null) 094 * @param date session date to use when creating progress sessions 095 * @param graph shared graph used to visualize recent results 096 */ 097 public CVI(String studentName, LocalDate date, JLineGraph graph) { 098 this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; 099 this.dateParam = date; 100 this.graph = graph; 101 setLayout(new BorderLayout()); 102 JPanel panel = new JPanel(new GridBagLayout()); 103 JPanel view = new JPanel(new BorderLayout()); 104 view.add(panel, BorderLayout.NORTH); 105 view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); 106 JScrollPane scroll = new JScrollPane(view); 107 GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.NORTHWEST; 108 109 JLabel title = new JLabel("CVI Progression"); 110 title.setFont(title.getFont().deriveFont(Font.BOLD,28f)); 111 title.getAccessibleContext().setAccessibleName("CVI Progression Title"); 112 title.setHorizontalAlignment(JLabel.LEFT); 113 gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=2; panel.add(title, gbc); 114 115 String[][] parts = new String[][]{{"P1_1","Color Preference"},{"P1_2","Need for Movement"},{"P1_3","Latency"},{"P1_4","Field Preference"},{"P1_5","Visual Complexity"},{"P1_6","Nonpurposeful Gaze"},{"P2_1","Distance Viewing"},{"P2_2","Atypical Reflexes"},{"P2_3","Visual Novelty"},{"P2_4","Visual Reach"}}; 116 String[] labels = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new); 117 int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(com.studentgui.uicomp.PhaseScoreField.getLabelFont(), labels); 118 com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(320, Math.max(140, maxPx + 50))); 119 int row = 1; 120 for (String[] pdef: parts) { 121 gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 2; 122 PhaseScoreField tf = new PhaseScoreField(pdef[1], 0); 123 tf.setToolTipText("Enter whole number score for " + pdef[1]); 124 tf.getAccessibleContext().setAccessibleName(pdef[1]); 125 tf.setName("cvi_" + pdef[0]); 126 panel.add(tf, gbc); 127 inputs.put(pdef[0], tf); 128 row++; 129 } 130 131 // Two side-by-side buttons: Submit Data (save + save PNG) and Open Latest Plot 132 gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.WEST; 133 JButton submit = new JButton("Submit Data"); 134 submit.setPreferredSize(new java.awt.Dimension(0, 32)); 135 submit.addActionListener((ActionEvent e) -> save()); 136 submit.setToolTipText("Save CVI assessment for selected student (Alt+S)"); 137 submit.setMnemonic(KeyEvent.VK_S); 138 submit.getAccessibleContext().setAccessibleName("Submit CVI Data"); 139 submit.setName("cvi_submit"); 140 panel.add(submit, gbc); 141 142 gbc.gridx = 1; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.WEST; 143 JButton openLatest = new JButton("Open Latest Plot"); 144 openLatest.setPreferredSize(new java.awt.Dimension(0, 32)); 145 openLatest.addActionListener((ActionEvent e) -> { 146 java.nio.file.Path plotPath = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "CVI"); 147 if (plotPath == null) com.studentgui.apphelpers.UiNotifier.show("No CVI plot found for student"); 148 else { 149 // Do not auto-open CVI plot on startup; only save it. Opening is handled 150 // by explicit user actions (Open Latest Plot). 151 } 152 }); 153 panel.add(openLatest, gbc); 154 gbc.gridwidth = 1; 155 156 add(scroll, BorderLayout.CENTER); add(graph, BorderLayout.SOUTH); 157 SwingUtilities.invokeLater(()->{ panel.setPreferredSize(panel.getPreferredSize()); revalidate(); }); 158 com.studentgui.apphelpers.Helpers.createFolderHierarchy(); 159 initParts(); 160 } 161 162 /** 163 * Ensure the CVI progress-type and part rows exist in the database. 164 */ 165 private void initParts() { 166 try { 167 int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("CVI"); 168 java.util.Set<String> keys = inputs.keySet(); 169 String[] codes = new String[keys.size()]; 170 int kidx = 0; 171 for (String k : keys) { 172 codes[kidx++] = k; 173 } 174 com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes); 175 } catch (SQLException ex) { 176 LOG.error("Error ensuring CVI parts", ex); 177 } 178 } 179 180 /** 181 * Validate inputs and persist them as a new CVI progress session for the 182 * selected student. 183 */ 184 private void save() { 185 if (studentNameParam == null || studentNameParam.trim().isEmpty()) { 186 com.studentgui.apphelpers.UiNotifier.show("Please select a student before saving CVI data."); 187 return; 188 } 189 190 try { 191 int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(studentNameParam); 192 int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("CVI"); 193 int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, dateParam); 194 java.util.Set<String> keys = inputs.keySet(); 195 String[] codes = new String[keys.size()]; 196 int kidx = 0; 197 for (String k : keys) codes[kidx++] = k; 198 int[] scores = new int[codes.length]; 199 for (int i = 0; i < codes.length; i++) { 200 scores[i] = inputs.get(codes[i]).getValue(); 201 } 202 com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); 203 LOG.info("CVI data saved for {}", studentNameParam); 204 com.studentgui.apphelpers.UiNotifier.show("CVI data saved."); 205 com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores); 206 java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "CVI", payload, sessionId); 207 if (jsonOut == null) LOG.warn("Unable to save CVI session JSON for sessionId={}", sessionId); 208 try { 209 java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam); 210 java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam); 211 java.nio.file.Files.createDirectories(plotsOut); 212 java.nio.file.Files.createDirectories(reportsOut); 213 java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; 214 String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString(); 215 String baseName = "CVI-" + sessionId + "-" + dateStr; 216 217 com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "CVI", Integer.MAX_VALUE); 218 java.util.Map<String, java.nio.file.Path> groups = null; 219 String[] labels = new String[codes.length]; 220 for (int i = 0; i < codes.length; i++) labels[i] = inputs.get(codes[i]).getLabel(); 221 if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { 222 graph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels); 223 groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240); 224 java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1); 225 dateStr = headerDate.format(df); 226 } else { 227 java.util.List<java.util.List<Integer>> rowsList = new java.util.ArrayList<>(); 228 java.util.List<Integer> latest = new java.util.ArrayList<>(); 229 for (String c : codes) { 230 latest.add(inputs.get(c).getValue()); 231 } 232 rowsList.add(latest); 233 graph.updateWithGroupedData(rowsList, codes); 234 groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240); 235 } 236 237 if (groups == null) groups = new java.util.LinkedHashMap<>(); 238 StringBuilder md = new StringBuilder(); 239 md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n"); 240 for (java.util.Map.Entry<String, java.nio.file.Path> e : groups.entrySet()) { 241 md.append("## ").append(e.getKey()).append("\n\n"); 242 md.append(".append(e.getValue().getFileName().toString()).append(")\n\n"); 243 } 244 java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md"); 245 java.nio.file.Files.writeString(mdFile, md.toString(), java.nio.charset.StandardCharsets.UTF_8); 246 247 try { 248 String[] palette = JLineGraph.PALETTE_HEX; 249 java.util.LinkedHashMap<String, java.util.List<Integer>> groupsIdx = new java.util.LinkedHashMap<>(); 250 for (int i = 0; i < codes.length; i++) { 251 String code = codes[i]; 252 String grp = code != null && code.contains("_") ? code.split("_")[0] : code; 253 groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); 254 } 255 StringBuilder html = new StringBuilder(); 256 html.append("<!doctype html><html><head><meta charset=\"utf-8\"><title>"); 257 html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append("</title>"); 258 html.append("<style>body{font-family:sans-serif;margin:20px;} img{max-width:100%;height:auto;border:1px solid #ccc;margin-bottom:8px;} .legend{max-height:160px;overflow:auto;border:1px solid #ddd;padding:8px;margin-bottom:24px;} .legend-item{display:flex;align-items:center;gap:8px;padding:4px 0;} .swatch{width:18px;height:12px;border:1px solid #333;display:inline-block}</style>"); 259 html.append("</head><body>"); 260 html.append("<h1>").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("</h1>"); 261 for (java.util.Map.Entry<String, java.nio.file.Path> e2 : groups.entrySet()) { 262 String grp = e2.getKey(); 263 String imgName = e2.getValue().getFileName().toString(); 264 html.append("<h2>").append(grp).append("</h2>"); 265 html.append("<div class=\"plot\"><img src=\"../plots/").append(imgName).append("\" alt=\"").append(grp).append("\"></div>"); 266 java.util.List<Integer> idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>()); 267 html.append("<div class=\"legend\">"); 268 for (int s = 0; s < idxs.size(); s++) { 269 int idx = idxs.get(s); 270 String code = codes[idx]; 271 String human = labels[idx]; 272 String seriesName = code + " - " + human; 273 String color = palette[s % palette.length]; 274 html.append("<div class=\"legend-item\">"); 275 html.append("<span class=\"swatch\" style=\"background:"); 276 html.append(color); 277 html.append(";\"></span>"); 278 html.append("<div>"); 279 html.append(seriesName); 280 html.append("</div></div>"); 281 } 282 html.append("</div>"); 283 } 284 html.append("</body></html>"); 285 java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html"); 286 java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8); 287 LOG.info("Wrote CVI HTML session report {}", htmlFile); 288 } catch (java.io.IOException ioex) { 289 LOG.warn("Unable to write CVI HTML report: {}", ioex.toString()); 290 } 291 } catch (java.io.IOException ioe) { 292 LOG.warn("Unable to save CVI per-phase plots or markdown report: {}", ioe.toString()); 293 } 294 } catch (SQLException ex) { 295 LOG.error("Error saving CVI data", ex); 296 com.studentgui.apphelpers.UiNotifier.show("Database error saving CVI data: " + ex.getMessage()); 297 } 298 } 299 300 // Plotting is handled as part of save(): the submit action updates the shared 301 // graph and writes a static PNG into the student's plots folder. 302}