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