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}