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