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