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                    // determine if line is a pickup or drop
133                    if ((!Setup.getPickupEnginePrefix().trim().isEmpty() &&
134                            line.startsWith(Setup.getPickupEnginePrefix() + TrainCommon.SPACE)) ||
135                            (!Setup.getPickupCarPrefix().trim().isEmpty() &&
136                                    line.startsWith(Setup.getPickupCarPrefix() + TrainCommon.SPACE)) ||
137                            (!Setup.getSwitchListPickupCarPrefix().trim().isEmpty() &&
138                                    line.startsWith(Setup.getSwitchListPickupCarPrefix() + TrainCommon.SPACE))) {
139                        c = Setup.getPickupColor();
140                    } else if ((!Setup.getDropEnginePrefix().trim().isEmpty() &&
141                            line.startsWith(Setup.getDropEnginePrefix() + TrainCommon.SPACE)) ||
142                            (!Setup.getDropCarPrefix().trim().isEmpty() &&
143                                    line.startsWith(Setup.getDropCarPrefix() + TrainCommon.SPACE)) ||
144                            (!Setup.getSwitchListDropCarPrefix().trim().isEmpty() &&
145                                    line.startsWith(Setup.getSwitchListDropCarPrefix() + TrainCommon.SPACE))) {
146                        c = Setup.getDropColor();
147                    } else if ((!Setup.getLocalPrefix().trim().isEmpty() &&
148                            line.startsWith(Setup.getLocalPrefix() + TrainCommon.SPACE)) ||
149                            (!Setup.getSwitchListLocalPrefix().trim().isEmpty() &&
150                                    line.startsWith(Setup.getSwitchListLocalPrefix() + TrainCommon.SPACE))) {
151                        c = Setup.getLocalColor();
152                    } else if (line.contains(TrainCommon.TEXT_COLOR_START)) {
153                        c = TrainCommon.getTextColor(line);
154                        if (line.contains(TrainCommon.TEXT_COLOR_END)) {
155                            printingColor = false;
156                        } else {
157                            // printing multiple lines in color
158                            printingColor = true;
159                        }
160                        // could be a color change when using two column format
161                        if (line.contains(Character.toString(VERTICAL_LINE_SEPARATOR))) {
162                            String s = line.substring(0, line.indexOf(VERTICAL_LINE_SEPARATOR));
163                            s = TrainCommon.getTextColorString(s);
164                            try {
165                                writer.write(c, s); // 1st half of line printed
166                            } catch (IOException e) {
167                                log.debug("Print write color failed");
168                                break;
169                            }
170                            // get the new color and text
171                            line = line.substring(line.indexOf(VERTICAL_LINE_SEPARATOR));
172                            c = TrainCommon.getTextColor(line);
173                            // pad out string
174                            StringBuffer sb = new StringBuffer();
175                            for (int i = 0; i < s.length(); i++) {
176                                sb.append(SPACE);
177                            }
178                            // 2nd half of line to be printed
179                            line = sb.append(TrainCommon.getTextColorString(line)).toString();
180                        } else {
181                            // simple case only one color
182                            line = TrainCommon.getTextColorString(line);
183                        }
184                    } else if (line.contains(TrainCommon.TEXT_COLOR_END)) {
185                        printingColor = false;
186                        line = TrainCommon.getTextColorString(line);
187                    } else if (!line.startsWith(TrainCommon.TAB) && !printingColor) {
188                        c = null;
189                    }
190                    for (int i = 0; i < line.length(); i++) {
191                        if (line.charAt(i) == VERTICAL_LINE_SEPARATOR) {
192                            // make a frame (two column format)
193                            if (Setup.isTabEnabled()) {
194                                writer.write(writer.getCurrentLineNumber(), 0, writer.getCurrentLineNumber() + 1, 0);
195                                writer.write(writer.getCurrentLineNumber(), line.length() + 1,
196                                        writer.getCurrentLineNumber() + 1, line.length() + 1);
197                            }
198                            writer.write(writer.getCurrentLineNumber(), i + 1, writer.getCurrentLineNumber() + 1,
199                                    i + 1);
200                        }
201                    }
202                    line = line.replace(VERTICAL_LINE_SEPARATOR, SPACE);
203
204                    if (c != null) {
205                        try {
206                            writer.write(c, line + NEW_LINE);
207                            continue;
208                        } catch (IOException e) {
209                            log.debug("Print write color failed");
210                            break;
211                        }
212                    }
213                }
214                try {
215                    writer.write(line + NEW_LINE);
216                } catch (IOException e) {
217                    log.debug("Print write failed");
218                    break;
219                }
220            }
221            try {
222                in.close();
223            } catch (IOException e) {
224                log.debug("Could not close in stream");
225            }
226
227            // and force completion of the printing
228            // close is no longer needed when using the try / catch declaration
229            // writer.close();
230        } catch (FileNotFoundException e) {
231            log.error("Build file doesn't exist", e);
232        } catch (HardcopyWriter.PrintCanceledException ex) {
233            log.debug("Print cancelled");
234        } catch (IOException e) {
235            log.warn("Exception printing: {}", e.getLocalizedMessage());
236        }
237    }
238
239    /**
240     * Creates a new build report file with the print detail numbers replaced by
241     * indentations. Then calls open desktop editor.
242     *
243     * @param file build file
244     * @param name train name
245     */
246    public static void editReport(File file, String name) {
247        // make a new file with the build report levels removed
248        File buildReport = InstanceManager.getDefault(TrainManagerXml.class)
249                .createTrainBuildReportFile(Bundle.getMessage("Report") + " " + name);
250        editReport(file, buildReport);
251        // open the file
252        TrainUtilities.openDesktop(buildReport);
253    }
254    
255    /**
256     * Creates a new build report file with the print detail numbers replaced by
257     * indentations.
258     * 
259     * @param file Raw file with detail level numbers
260     * @param fileOut Formated file with indentations
261     */
262    public static void editReport(File file, File fileOut) {
263
264        try (BufferedReader in = new BufferedReader(new InputStreamReader(
265                new FileInputStream(file), StandardCharsets.UTF_8));
266                PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(
267                        new FileOutputStream(fileOut), StandardCharsets.UTF_8)), true);) {
268
269            String line;
270            while (true) {
271                try {
272                    line = in.readLine();
273                    if (line == null) {
274                        break;
275                    }
276                    line = filterBuildReport(line, Setup.isBuildReportIndentEnabled());
277                    if (line.isEmpty()) {
278                        continue;
279                    }
280                    out.println(line); // indent lines for each level
281                } catch (IOException e) {
282                    log.debug("Print read failed");
283                    break;
284                }
285            }
286            // and force completion of the printing
287            try {
288                in.close();
289            } catch (IOException e) {
290                log.debug("Close failed");
291            }
292            out.close();
293        } catch (FileNotFoundException e) {
294            log.error("Build file doesn't exist: {}", e.getLocalizedMessage());
295        } catch (IOException e) {
296            log.error("Can not create build report file: {}", e.getLocalizedMessage());
297        }
298    }
299
300    /*
301     * Removes the print levels from the build report
302     */
303    private static String filterBuildReport(String line, boolean indent) {
304        String[] inputLine = line.split("\\s+"); // NOI18N
305        if (inputLine.length == 0) {
306            return "";
307        }
308        if (inputLine[0].equals(Setup.BUILD_REPORT_VERY_DETAILED + TrainCommon.BUILD_REPORT_CHAR) ||
309                inputLine[0].equals(Setup.BUILD_REPORT_DETAILED + TrainCommon.BUILD_REPORT_CHAR) ||
310                inputLine[0].equals(Setup.BUILD_REPORT_NORMAL + TrainCommon.BUILD_REPORT_CHAR) ||
311                inputLine[0].equals(Setup.BUILD_REPORT_MINIMAL + TrainCommon.BUILD_REPORT_CHAR)) {
312
313            if (Setup.getBuildReportLevel().equals(Setup.BUILD_REPORT_MINIMAL)) {
314                if (inputLine[0].equals(Setup.BUILD_REPORT_NORMAL + TrainCommon.BUILD_REPORT_CHAR) ||
315                        inputLine[0].equals(Setup.BUILD_REPORT_DETAILED + TrainCommon.BUILD_REPORT_CHAR) ||
316                        inputLine[0].equals(Setup.BUILD_REPORT_VERY_DETAILED + TrainCommon.BUILD_REPORT_CHAR)) {
317                    return ""; // don't print this line
318                }
319            }
320            if (Setup.getBuildReportLevel().equals(Setup.BUILD_REPORT_NORMAL)) {
321                if (inputLine[0].equals(Setup.BUILD_REPORT_DETAILED + TrainCommon.BUILD_REPORT_CHAR) ||
322                        inputLine[0].equals(Setup.BUILD_REPORT_VERY_DETAILED + TrainCommon.BUILD_REPORT_CHAR)) {
323                    return ""; // don't print this line
324                }
325            }
326            if (Setup.getBuildReportLevel().equals(Setup.BUILD_REPORT_DETAILED)) {
327                if (inputLine[0].equals(Setup.BUILD_REPORT_VERY_DETAILED + TrainCommon.BUILD_REPORT_CHAR)) {
328                    return ""; // don't print this line
329                }
330            }
331            // do not indent if false
332            int start = 0;
333            if (indent) {
334                // indent lines based on level
335                if (inputLine[0].equals(Setup.BUILD_REPORT_VERY_DETAILED + TrainCommon.BUILD_REPORT_CHAR)) {
336                    inputLine[0] = "   ";
337                } else if (inputLine[0].equals(Setup.BUILD_REPORT_DETAILED + TrainCommon.BUILD_REPORT_CHAR)) {
338                    inputLine[0] = "  ";
339                } else if (inputLine[0].equals(Setup.BUILD_REPORT_NORMAL + TrainCommon.BUILD_REPORT_CHAR)) {
340                    inputLine[0] = " ";
341                } else if (inputLine[0].equals(Setup.BUILD_REPORT_MINIMAL + TrainCommon.BUILD_REPORT_CHAR)) {
342                    inputLine[0] = "";
343                }
344            } else {
345                start = 1;
346            }
347            // rebuild line
348            StringBuffer buf = new StringBuffer();
349            for (int i = start; i < inputLine.length; i++) {
350                buf.append(inputLine[i] + " ");
351            }
352            // blank line?
353            if (buf.length() == 0) {
354                return " ";
355            }
356            return buf.toString();
357        } else {
358            log.debug("ERROR first characters of build report not valid ({})", line);
359            return "ERROR " + line; // NOI18N
360        }
361    }
362
363    public static JComboBox<String> getPrinterJComboBox() {
364        JComboBox<String> box = new JComboBox<>();
365        PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
366        for (PrintService printService : services) {
367            box.addItem(printService.getName());
368        }
369
370        // Set to default printer
371        box.setSelectedItem(getDefaultPrinterName());
372
373        return box;
374    }
375
376    public static String getDefaultPrinterName() {
377        if (PrintServiceLookup.lookupDefaultPrintService() != null) {
378            return PrintServiceLookup.lookupDefaultPrintService().getName();
379        }
380        return ""; // no default printer specified
381    }
382
383    private final static Logger log = LoggerFactory.getLogger(TrainPrintUtilities.class);
384}