001package jmri.jmrit.operations.trains; 002 003import java.awt.*; 004import java.awt.JobAttributes.SidesType; 005import java.io.*; 006import java.nio.charset.StandardCharsets; 007 008import javax.print.PrintService; 009import javax.print.PrintServiceLookup; 010import javax.swing.*; 011 012import org.slf4j.Logger; 013import org.slf4j.LoggerFactory; 014 015import jmri.InstanceManager; 016import jmri.jmrit.operations.setup.Setup; 017import jmri.util.davidflanagan.HardcopyWriter; 018 019/** 020 * Train print utilities. Used for train Manifests and build reports. 021 * 022 * @author Daniel Boudreau (C) 2010, 2025 023 */ 024public class TrainPrintUtilities extends TrainCommon { 025 026 static final char SPACE = ' '; 027 028 /** 029 * Print or preview a train Manifest, build report, or switch list. 030 * 031 * @param file File to be printed or previewed 032 * @param name Title of document 033 * @param isPreview true if preview 034 * @param fontName optional font to use when printing document 035 * @param isBuildReport true if build report 036 * @param logoURL optional pathname for logo 037 * @param printerName optional default printer name 038 * @param orientation Setup.LANDSCAPE, Setup.PORTRAIT, or Setup.HANDHELD 039 * @param fontSize font size 040 * @param isPrintHeader when true print page header 041 * @param sidesType two sides long or short can be null 042 */ 043 public static void printReport(File file, String name, boolean isPreview, String fontName, boolean isBuildReport, 044 String logoURL, String printerName, String orientation, int fontSize, boolean isPrintHeader, 045 SidesType sidesType) { 046 // obtain a HardcopyWriter to do this 047 048 boolean isLandScape = false; 049 double margin = .5; 050 Dimension pagesize = null; // HardcopyWritter provides default page 051 // sizes for portrait and landscape 052 if (orientation.equals(Setup.LANDSCAPE)) { 053 margin = .65; 054 isLandScape = true; 055 } 056 if (orientation.equals(Setup.HANDHELD) || orientation.equals(Setup.HALFPAGE)) { 057 isPrintHeader = false; 058 // add margins to page size 059 pagesize = new Dimension(getPageSize(orientation).width + PAPER_MARGINS.width, 060 getPageSize(orientation).height + PAPER_MARGINS.height); 061 } 062 try (HardcopyWriter writer = new HardcopyWriter(new Frame(), name, fontSize, margin, 063 margin, .5, .5, isPreview, printerName, isLandScape, isPrintHeader, sidesType, pagesize); 064 BufferedReader in = new BufferedReader(new InputStreamReader( 065 new FileInputStream(file), StandardCharsets.UTF_8));) { 066 067 // set font 068 if (!fontName.isEmpty()) { 069 writer.setFontName(fontName); 070 } 071 072 if (!isBuildReport && logoURL != null && !logoURL.equals(Setup.NONE)) { 073 ImageIcon icon = new ImageIcon(logoURL); 074 if (icon.getIconWidth() == -1) { 075 log.error("Logo not found: {}", logoURL); 076 } else { 077 writer.write(icon.getImage(), new JLabel(icon)); 078 } 079 } 080 081 String line; 082 Color color = null; 083 boolean printingColor = false; 084 while (true) { 085 try { 086 line = in.readLine(); 087 } catch (IOException e) { 088 log.debug("Print read failed"); 089 break; 090 } 091 if (line == null) { 092 if (isPreview) { 093 // need to do this in case the input file was empty to create preview 094 writer.write(" "); 095 } 096 break; 097 } 098 // check for build report print level 099 if (isBuildReport) { 100 line = filterBuildReport(line, false); // no indent 101 if (line.isEmpty()) { 102 continue; 103 } 104 } else { 105 // printing the train Manifest or switch list 106 // determine if there's a line separator 107 if (printHorizontialLineSeparator(writer, line)) { 108 color = null; 109 continue; 110 } 111 // color text? 112 if (line.contains(TEXT_COLOR_START)) { 113 color = getTextColor(line); 114 if (line.contains(TEXT_COLOR_END)) { 115 printingColor = false; 116 } else { 117 // printing multiple lines in color 118 printingColor = true; 119 } 120 // could be a color change when using two column format 121 if (line.contains(Character.toString(VERTICAL_LINE_CHAR))) { 122 String s = line.substring(0, line.indexOf(VERTICAL_LINE_CHAR)); 123 s = getTextColorString(s); 124 writer.write(color, s); // 1st half of line printed 125 // get the new color and text 126 line = line.substring(line.indexOf(VERTICAL_LINE_CHAR)); 127 color = getTextColor(line); 128 // pad out string 129 line = tabString(getTextColorString(line), s.length()); 130 } else { 131 // simple case only one color 132 line = getTextColorString(line); 133 } 134 } else if (line.contains(TEXT_COLOR_END)) { 135 printingColor = false; 136 line = getTextColorString(line); 137 } else if (!line.startsWith(TAB) && !printingColor) { 138 color = null; 139 } 140 141 printVerticalLineSeparator(writer, line); 142 line = line.replace(VERTICAL_LINE_CHAR, SPACE); 143 144 if (color != null) { 145 writer.write(color, line + NEW_LINE); 146 continue; 147 } 148 } 149 writer.write(line + NEW_LINE); 150 } 151 in.close(); 152 } catch (FileNotFoundException e) { 153 log.error("Build file doesn't exist", e); 154 } catch (HardcopyWriter.PrintCanceledException ex) { 155 log.debug("Print cancelled"); 156 } catch (IOException e) { 157 log.warn("Exception printing: {}", e.getLocalizedMessage()); 158 } 159 } 160 161 /* 162 * Returns true if horizontal line was printed, or line length = 0 163 */ 164 private static boolean printHorizontialLineSeparator(HardcopyWriter writer, String line) { 165 boolean horizontialLineSeparatorFound = true; 166 if (line.length() > 0) { 167 for (int i = 0; i < line.length(); i++) { 168 if (line.charAt(i) != HORIZONTAL_LINE_CHAR) { 169 horizontialLineSeparatorFound = false; 170 break; 171 } 172 } 173 if (horizontialLineSeparatorFound) { 174 writer.write(writer.getCurrentLineNumber(), 0, writer.getCurrentLineNumber(), 175 line.length() + 1); 176 } 177 } 178 return horizontialLineSeparatorFound; 179 } 180 181 private static void printVerticalLineSeparator(HardcopyWriter writer, String line) { 182 for (int i = 0; i < line.length(); i++) { 183 if (line.charAt(i) == VERTICAL_LINE_CHAR) { 184 // make a frame (two column format) 185 if (Setup.isTabEnabled()) { 186 writer.write(writer.getCurrentLineNumber(), 0, writer.getCurrentLineNumber() + 1, 0); 187 writer.write(writer.getCurrentLineNumber(), line.length() + 1, 188 writer.getCurrentLineNumber() + 1, line.length() + 1); 189 } 190 writer.write(writer.getCurrentLineNumber(), i + 1, writer.getCurrentLineNumber() + 1, 191 i + 1); 192 } 193 } 194 } 195 196 /** 197 * Creates a new build report file with the print detail numbers replaced by 198 * indentations. Then calls open desktop editor. 199 * 200 * @param file build file 201 * @param name train name 202 */ 203 public static void editReport(File file, String name) { 204 // make a new file with the build report levels removed 205 File buildReport = InstanceManager.getDefault(TrainManagerXml.class) 206 .createTrainBuildReportFile(Bundle.getMessage("Report") + " " + name); 207 editReport(file, buildReport); 208 // open the file 209 TrainUtilities.openDesktop(buildReport); 210 } 211 212 /** 213 * Creates a new build report file with the print detail numbers replaced by 214 * indentations. 215 * 216 * @param file Raw file with detail level numbers 217 * @param fileOut Formated file with indentations 218 */ 219 public static void editReport(File file, File fileOut) { 220 221 try (BufferedReader in = new BufferedReader(new InputStreamReader( 222 new FileInputStream(file), StandardCharsets.UTF_8)); 223 PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter( 224 new FileOutputStream(fileOut), StandardCharsets.UTF_8)), true);) { 225 226 String line; 227 while (true) { 228 try { 229 line = in.readLine(); 230 if (line == null) { 231 break; 232 } 233 line = filterBuildReport(line, Setup.isBuildReportIndentEnabled()); 234 if (line.isEmpty()) { 235 continue; 236 } 237 out.println(line); // indent lines for each level 238 } catch (IOException e) { 239 log.debug("Print read failed"); 240 break; 241 } 242 } 243 in.close(); 244 } catch (FileNotFoundException e) { 245 log.error("Build file doesn't exist: {}", e.getLocalizedMessage()); 246 } catch (IOException e) { 247 log.error("Can not create build report file: {}", e.getLocalizedMessage()); 248 } 249 } 250 251 /* 252 * Removes the print levels from the build report 253 */ 254 private static String filterBuildReport(String line, boolean indent) { 255 String[] inputLine = line.split("\\s+"); // NOI18N 256 if (inputLine.length == 0) { 257 return ""; 258 } 259 if (inputLine[0].equals(Setup.BUILD_REPORT_VERY_DETAILED + BUILD_REPORT_CHAR) || 260 inputLine[0].equals(Setup.BUILD_REPORT_DETAILED + BUILD_REPORT_CHAR) || 261 inputLine[0].equals(Setup.BUILD_REPORT_NORMAL + BUILD_REPORT_CHAR) || 262 inputLine[0].equals(Setup.BUILD_REPORT_MINIMAL + BUILD_REPORT_CHAR)) { 263 264 if (Setup.getBuildReportLevel().equals(Setup.BUILD_REPORT_MINIMAL)) { 265 if (inputLine[0].equals(Setup.BUILD_REPORT_NORMAL + BUILD_REPORT_CHAR) || 266 inputLine[0].equals(Setup.BUILD_REPORT_DETAILED + BUILD_REPORT_CHAR) || 267 inputLine[0].equals(Setup.BUILD_REPORT_VERY_DETAILED + BUILD_REPORT_CHAR)) { 268 return ""; // don't print this line 269 } 270 } 271 if (Setup.getBuildReportLevel().equals(Setup.BUILD_REPORT_NORMAL)) { 272 if (inputLine[0].equals(Setup.BUILD_REPORT_DETAILED + BUILD_REPORT_CHAR) || 273 inputLine[0].equals(Setup.BUILD_REPORT_VERY_DETAILED + BUILD_REPORT_CHAR)) { 274 return ""; // don't print this line 275 } 276 } 277 if (Setup.getBuildReportLevel().equals(Setup.BUILD_REPORT_DETAILED)) { 278 if (inputLine[0].equals(Setup.BUILD_REPORT_VERY_DETAILED + BUILD_REPORT_CHAR)) { 279 return ""; // don't print this line 280 } 281 } 282 // do not indent if false 283 int start = 0; 284 if (indent) { 285 // indent lines based on level 286 if (inputLine[0].equals(Setup.BUILD_REPORT_VERY_DETAILED + BUILD_REPORT_CHAR)) { 287 inputLine[0] = " "; 288 } else if (inputLine[0].equals(Setup.BUILD_REPORT_DETAILED + BUILD_REPORT_CHAR)) { 289 inputLine[0] = " "; 290 } else if (inputLine[0].equals(Setup.BUILD_REPORT_NORMAL + BUILD_REPORT_CHAR)) { 291 inputLine[0] = " "; 292 } else if (inputLine[0].equals(Setup.BUILD_REPORT_MINIMAL + BUILD_REPORT_CHAR)) { 293 inputLine[0] = ""; 294 } 295 } else { 296 start = 1; 297 } 298 // rebuild line 299 StringBuffer buf = new StringBuffer(); 300 for (int i = start; i < inputLine.length; i++) { 301 buf.append(inputLine[i] + " "); 302 } 303 // blank line? 304 if (buf.length() == 0) { 305 return " "; 306 } 307 return buf.toString(); 308 } else { 309 log.debug("ERROR first characters of build report not valid ({})", line); 310 return "ERROR " + line; // NOI18N 311 } 312 } 313 314 public static JComboBox<String> getPrinterJComboBox() { 315 JComboBox<String> box = new JComboBox<>(); 316 PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null); 317 for (PrintService printService : services) { 318 box.addItem(printService.getName()); 319 } 320 321 // Set to default printer 322 box.setSelectedItem(getDefaultPrinterName()); 323 324 return box; 325 } 326 327 public static String getDefaultPrinterName() { 328 if (PrintServiceLookup.lookupDefaultPrintService() != null) { 329 return PrintServiceLookup.lookupDefaultPrintService().getName(); 330 } 331 return ""; // no default printer specified 332 } 333 334 private final static Logger log = LoggerFactory.getLogger(TrainPrintUtilities.class); 335}