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