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}