001package jmri.jmrit.operations.trains.trainbuilder;
002
003import java.awt.*;
004import java.io.PrintWriter;
005import java.text.MessageFormat;
006import java.text.SimpleDateFormat;
007import java.util.*;
008import java.util.List;
009
010import javax.swing.JLabel;
011
012import org.slf4j.Logger;
013import org.slf4j.LoggerFactory;
014
015import com.fasterxml.jackson.databind.util.StdDateFormat;
016
017import jmri.InstanceManager;
018import jmri.jmrit.operations.locations.*;
019import jmri.jmrit.operations.locations.divisions.DivisionManager;
020import jmri.jmrit.operations.rollingstock.RollingStock;
021import jmri.jmrit.operations.rollingstock.cars.*;
022import jmri.jmrit.operations.rollingstock.engines.*;
023import jmri.jmrit.operations.routes.RouteLocation;
024import jmri.jmrit.operations.setup.Control;
025import jmri.jmrit.operations.setup.Setup;
026import jmri.jmrit.operations.trains.*;
027import jmri.util.ColorUtil;
028import jmri.util.davidflanagan.HardcopyWriter;
029
030/**
031 * Common routines for trains
032 *
033 * @author Daniel Boudreau (C) Copyright 2008, 2009, 2010, 2011, 2012, 2013,
034 *         2021, 2025
035 */
036public class TrainCommon {
037
038    protected static final String TAB = "    "; // NOI18N
039    protected static final String NEW_LINE = "\n"; // NOI18N
040    public static final String SPACE = " ";
041    public static final String BLANK_LINE = " ";
042    protected static final char HORIZONTAL_LINE_CHAR = '-';
043    protected static final String BUILD_REPORT_CHAR = "-";
044    public static final String HYPHEN = "-";
045    protected static final char VERTICAL_LINE_CHAR = '|';
046    protected static final String TEXT_COLOR_START = "<FONT color=\"";
047    protected static final String TEXT_COLOR_DONE = "\">";
048    protected static final String TEXT_COLOR_END = "</FONT>";
049
050    // when true a pick up, when false a set out
051    protected static final boolean PICKUP = true;
052    // when true Manifest, when false switch list
053    protected static final boolean IS_MANIFEST = true;
054    // when true local car move
055    public static final boolean LOCAL = true;
056    // when true engine attribute, when false car
057    protected static final boolean ENGINE = true;
058    // when true, two column table is sorted by track names
059    public static final boolean IS_TWO_COLUMN_TRACK = true;
060
061    protected CarManager carManager = InstanceManager.getDefault(CarManager.class);
062    protected EngineManager engineManager = InstanceManager.getDefault(EngineManager.class);
063    protected LocationManager locationManager = InstanceManager.getDefault(LocationManager.class);
064
065    // for switch lists
066    protected boolean _pickupCars; // true when there are pickups
067    protected boolean _dropCars; // true when there are set outs
068
069    /**
070     * Used to generate "Two Column" format for engines.
071     *
072     * @param file       Manifest or Switch List File
073     * @param engineList List of engines for this train.
074     * @param rl         The RouteLocation being printed.
075     * @param isManifest True if manifest, false if switch list.
076     */
077    protected void blockLocosTwoColumn(PrintWriter file, List<Engine> engineList, RouteLocation rl,
078            boolean isManifest) {
079        if (isThereWorkAtLocation(null, engineList, rl)) {
080            printEngineHeader(file, isManifest);
081        }
082        int lineLength = getLineLength(isManifest);
083        for (Engine engine : engineList) {
084            if (engine.getRouteLocation() == rl && !engine.getTrackName().equals(Engine.NONE)) {
085                String pullText = padAndTruncate(pickupEngine(engine).trim(), lineLength / 2);
086                pullText = formatColorString(pullText, Setup.getPickupEngineColor());
087                String s = pullText + VERTICAL_LINE_CHAR + tabString("", lineLength / 2 - 1);
088                addLine(file, s);
089            }
090            if (engine.getRouteDestination() == rl) {
091                String dropText = padAndTruncate(dropEngine(engine).trim(), lineLength / 2 - 1);
092                dropText = formatColorString(dropText, Setup.getDropEngineColor());
093                String s = tabString("", lineLength / 2) + VERTICAL_LINE_CHAR + dropText;
094                addLine(file, s);
095            }
096        }
097    }
098
099    /**
100     * Adds a list of locomotive pick ups for the route location to the output
101     * file. Used to generate "Standard" format.
102     *
103     * @param file       Manifest or Switch List File
104     * @param engineList List of engines for this train.
105     * @param rl         The RouteLocation being printed.
106     * @param isManifest True if manifest, false if switch list
107     */
108    protected void pickupEngines(PrintWriter file, List<Engine> engineList, RouteLocation rl, boolean isManifest) {
109        boolean printHeader = Setup.isPrintHeadersEnabled();
110        for (Engine engine : engineList) {
111            if (engine.getRouteLocation() == rl && !engine.getTrackName().equals(Engine.NONE)) {
112                if (printHeader) {
113                    printPickupEngineHeader(file, isManifest);
114                    printHeader = false;
115                }
116                pickupEngine(file, engine, isManifest);
117            }
118        }
119    }
120
121    private void pickupEngine(PrintWriter file, Engine engine, boolean isManifest) {
122        StringBuffer buf = new StringBuffer(padAndTruncateIfNeeded(Setup.getPickupEnginePrefix(),
123                isManifest ? Setup.getManifestPrefixLength() : Setup.getSwitchListPrefixLength()));
124        String[] format = Setup.getPickupEngineMessageFormat();
125        for (String attribute : format) {
126            String s = getEngineAttribute(engine, attribute, PICKUP);
127            if (!checkStringLength(buf.toString() + s, isManifest)) {
128                addLine(file, buf, Setup.getPickupEngineColor());
129                buf = new StringBuffer(TAB); // new line
130            }
131            buf.append(s);
132        }
133        addLine(file, buf, Setup.getPickupEngineColor());
134    }
135
136    /**
137     * Adds a list of locomotive drops for the route location to the output
138     * file. Used to generate "Standard" format.
139     *
140     * @param file       Manifest or Switch List File
141     * @param engineList List of engines for this train.
142     * @param rl         The RouteLocation being printed.
143     * @param isManifest True if manifest, false if switch list
144     */
145    protected void dropEngines(PrintWriter file, List<Engine> engineList, RouteLocation rl, boolean isManifest) {
146        boolean printHeader = Setup.isPrintHeadersEnabled();
147        for (Engine engine : engineList) {
148            if (engine.getRouteDestination() == rl) {
149                if (printHeader) {
150                    printDropEngineHeader(file, isManifest);
151                    printHeader = false;
152                }
153                dropEngine(file, engine, isManifest);
154            }
155        }
156    }
157
158    private void dropEngine(PrintWriter file, Engine engine, boolean isManifest) {
159        StringBuffer buf = new StringBuffer(padAndTruncateIfNeeded(Setup.getDropEnginePrefix(),
160                isManifest ? Setup.getManifestPrefixLength() : Setup.getSwitchListPrefixLength()));
161        String[] format = Setup.getDropEngineMessageFormat();
162        for (String attribute : format) {
163            String s = getEngineAttribute(engine, attribute, !PICKUP);
164            if (!checkStringLength(buf.toString() + s, isManifest)) {
165                addLine(file, buf, Setup.getDropEngineColor());
166                buf = new StringBuffer(TAB); // new line
167            }
168            buf.append(s);
169        }
170        addLine(file, buf, Setup.getDropEngineColor());
171    }
172
173    /**
174     * Returns the pick up string for a loco. Useful for frames like the train
175     * conductor and yardmaster.
176     *
177     * @param engine The Engine.
178     * @return engine pick up string
179     */
180    public String pickupEngine(Engine engine) {
181        StringBuilder builder = new StringBuilder();
182        for (String attribute : Setup.getPickupEngineMessageFormat()) {
183            builder.append(getEngineAttribute(engine, attribute, PICKUP));
184        }
185        return builder.toString();
186    }
187
188    /**
189     * Returns the drop string for a loco. Useful for frames like the train
190     * conductor and yardmaster.
191     *
192     * @param engine The Engine.
193     * @return engine drop string
194     */
195    public String dropEngine(Engine engine) {
196        StringBuilder builder = new StringBuilder();
197        for (String attribute : Setup.getDropEngineMessageFormat()) {
198            builder.append(getEngineAttribute(engine, attribute, !PICKUP));
199        }
200        return builder.toString();
201    }
202
203    // the next three booleans are used to limit the header to once per location
204    boolean _printPickupHeader = true;
205    boolean _printSetoutHeader = true;
206    boolean _printLocalMoveHeader = true;
207
208    /**
209     * Block cars by track, then pick up and set out for each location in a
210     * train's route. This routine is used for the "Standard" format.
211     *
212     * @param file        Manifest or switch list File
213     * @param train       The train being printed.
214     * @param carList     List of cars for this train
215     * @param rl          The RouteLocation being printed
216     * @param printHeader True if new location.
217     * @param isManifest  True if manifest, false if switch list.
218     */
219    protected void blockCarsByTrack(PrintWriter file, Train train, List<Car> carList, RouteLocation rl,
220            boolean printHeader, boolean isManifest) {
221        if (printHeader) {
222            _printPickupHeader = true;
223            _printSetoutHeader = true;
224            _printLocalMoveHeader = true;
225        }
226        List<Track> tracks = rl.getLocation().getTracksByNameList(null);
227        List<String> trackNames = new ArrayList<>();
228        clearUtilityCarTypes(); // list utility cars by quantity
229        for (Track track : tracks) {
230            if (trackNames.contains(track.getSplitName())) {
231                continue;
232            }
233            trackNames.add(track.getSplitName()); // use a track name once
234
235            // car pick ups
236            blockCarsPickups(file, train, carList, rl, track, isManifest);
237
238            // now do car set outs and local moves
239            // group local moves first?
240            blockCarsSetoutsAndMoves(file, train, carList, rl, track, isManifest, false,
241                    Setup.isGroupCarMovesEnabled());
242            // set outs or both
243            blockCarsSetoutsAndMoves(file, train, carList, rl, track, isManifest, true,
244                    !Setup.isGroupCarMovesEnabled());
245
246            if (!Setup.isSortByTrackNameEnabled()) {
247                break; // done
248            }
249        }
250    }
251
252    private void blockCarsPickups(PrintWriter file, Train train, List<Car> carList, RouteLocation rl,
253            Track track, boolean isManifest) {
254        for (RouteLocation rld : train.getTrainBlockingOrder()) {
255            for (Car car : carList) {
256                if (Setup.isSortByTrackNameEnabled() &&
257                        !track.getSplitName().equals(car.getSplitTrackName())) {
258                    continue;
259                }
260                // Block cars
261                // caboose or FRED is placed at end of the train
262                // passenger cars are already blocked in the car list
263                // passenger cars with negative block numbers are placed at
264                // the front of the train, positive numbers at the end of
265                // the train.
266                if (isNextCar(car, rl, rld)) {
267                    // determine if pick up header is needed
268                    printPickupCarHeader(file, car, isManifest, !IS_TWO_COLUMN_TRACK);
269
270                    // use truncated format if there's a switch list
271                    boolean isTruncate = Setup.isPrintTruncateManifestEnabled() &&
272                            rl.getLocation().isSwitchListEnabled();
273
274                    if (car.isUtility()) {
275                        pickupUtilityCars(file, carList, car, isTruncate, isManifest);
276                    } else if (isManifest && isTruncate) {
277                        pickUpCarTruncated(file, car, isManifest);
278                    } else {
279                        pickUpCar(file, car, isManifest);
280                    }
281                    _pickupCars = true;
282                }
283            }
284        }
285    }
286
287    private void blockCarsSetoutsAndMoves(PrintWriter file, Train train, List<Car> carList, RouteLocation rl,
288            Track track, boolean isManifest, boolean isSetout, boolean isLocalMove) {
289        for (Car car : carList) {
290            if (!car.isLocalMove() && isSetout || car.isLocalMove() && isLocalMove) {
291                if (Setup.isSortByTrackNameEnabled() &&
292                        car.getRouteLocation() != null &&
293                        car.getRouteDestination() == rl) {
294                    // must sort local moves by car's destination track name and not car's track name
295                    // sorting by car's track name fails if there are "similar" location names.
296                    if (!track.getSplitName().equals(car.getSplitDestinationTrackName())) {
297                        continue;
298                    }
299                }
300                if (car.getRouteDestination() == rl && car.getDestinationTrack() != null) {
301                    // determine if drop or move header is needed
302                    printDropOrMoveCarHeader(file, car, isManifest, !IS_TWO_COLUMN_TRACK);
303
304                    // use truncated format if there's a switch list
305                    boolean isTruncate = Setup.isPrintTruncateManifestEnabled() &&
306                            rl.getLocation().isSwitchListEnabled() &&
307                            !train.isLocalSwitcher();
308
309                    if (car.isUtility()) {
310                        setoutUtilityCars(file, carList, car, isTruncate, isManifest);
311                    } else if (isManifest && isTruncate) {
312                        truncatedDropCar(file, car, isManifest);
313                    } else {
314                        dropCar(file, car, isManifest);
315                    }
316                    _dropCars = true;
317                }
318            }
319        }
320    }
321
322    /**
323     * Used to determine if car is the next to be processed when producing
324     * Manifests or Switch Lists. Caboose or FRED is placed at end of the train
325     * unless they are also passenger cars. Passenger cars are already blocked
326     * in the car list. Passenger cars with negative block numbers are placed at
327     * the front of the train, positive numbers at the end of the train. Note
328     * that a car in train doesn't have a track assignment.
329     * 
330     * @param car the car being tested
331     * @param rl  when in train's route the car is being pulled
332     * @param rld the destination being tested
333     * @return true if this car is the next one to be processed
334     */
335    public static boolean isNextCar(Car car, RouteLocation rl, RouteLocation rld) {
336        return isNextCar(car, rl, rld, false);
337    }
338        
339    public static boolean isNextCar(Car car, RouteLocation rl, RouteLocation rld, boolean isIgnoreTrack) {
340        Train train = car.getTrain();
341        if (train != null &&
342                (car.getTrack() != null || isIgnoreTrack) &&
343                car.getRouteLocation() == rl &&
344                (rld == car.getRouteDestination() &&
345                        !car.isCaboose() &&
346                        !car.hasFred() &&
347                        !car.isPassenger() ||
348                        car.isPassenger() &&
349                                car.getBlocking() < 0 &&
350                                rld == train.getRoute().getBlockingLocationFrontOfTrain() ||
351                        (car.isCaboose() && !car.isPassenger() ||
352                                car.hasFred() && !car.isPassenger() ||
353                                car.isPassenger() && car.getBlocking() >= 0) &&
354                                rld == train.getRoute().getBlockingLocationRearOfTrain())) {
355            return true;
356        }
357        return false;
358    }
359
360    private void printPickupCarHeader(PrintWriter file, Car car, boolean isManifest, boolean isTwoColumnTrack) {
361        if (_printPickupHeader && !car.isLocalMove()) {
362            printPickupCarHeader(file, isManifest, !IS_TWO_COLUMN_TRACK);
363            _printPickupHeader = false;
364            // check to see if the other headers are needed. If
365            // they are identical, not needed
366            if (getPickupCarHeader(isManifest, !IS_TWO_COLUMN_TRACK)
367                    .equals(getDropCarHeader(isManifest, !IS_TWO_COLUMN_TRACK))) {
368                _printSetoutHeader = false;
369            }
370            if (getPickupCarHeader(isManifest, !IS_TWO_COLUMN_TRACK)
371                    .equals(getLocalMoveHeader(isManifest))) {
372                _printLocalMoveHeader = false;
373            }
374        }
375    }
376
377    private void printDropOrMoveCarHeader(PrintWriter file, Car car, boolean isManifest, boolean isTwoColumnTrack) {
378        if (_printSetoutHeader && !car.isLocalMove()) {
379            printDropCarHeader(file, isManifest, !IS_TWO_COLUMN_TRACK);
380            _printSetoutHeader = false;
381            // check to see if the other headers are needed. If they
382            // are identical, not needed
383            if (getPickupCarHeader(isManifest, !IS_TWO_COLUMN_TRACK)
384                    .equals(getDropCarHeader(isManifest, !IS_TWO_COLUMN_TRACK))) {
385                _printPickupHeader = false;
386            }
387            if (getDropCarHeader(isManifest, !IS_TWO_COLUMN_TRACK).equals(getLocalMoveHeader(isManifest))) {
388                _printLocalMoveHeader = false;
389            }
390        }
391        if (_printLocalMoveHeader && car.isLocalMove()) {
392            printLocalCarMoveHeader(file, isManifest);
393            _printLocalMoveHeader = false;
394            // check to see if the other headers are needed. If they
395            // are identical, not needed
396            if (getPickupCarHeader(isManifest, !IS_TWO_COLUMN_TRACK)
397                    .equals(getLocalMoveHeader(isManifest))) {
398                _printPickupHeader = false;
399            }
400            if (getDropCarHeader(isManifest, !IS_TWO_COLUMN_TRACK).equals(getLocalMoveHeader(isManifest))) {
401                _printSetoutHeader = false;
402            }
403        }
404    }
405
406    /**
407     * Produces a two column format for car pick ups and set outs. Sorted by
408     * track and then by blocking order. This routine is used for the "Two
409     * Column" format.
410     *
411     * @param file        Manifest or switch list File
412     * @param train       The train
413     * @param carList     List of cars for this train
414     * @param rl          The RouteLocation being printed
415     * @param printHeader True if new location.
416     * @param isManifest  True if manifest, false if switch list.
417     */
418    protected void blockCarsTwoColumn(PrintWriter file, Train train, List<Car> carList, RouteLocation rl,
419            boolean printHeader, boolean isManifest) {
420        index = 0;
421        int lineLength = getLineLength(isManifest);
422        List<Track> tracks = rl.getLocation().getTracksByNameList(null);
423        List<String> trackNames = new ArrayList<>();
424        clearUtilityCarTypes(); // list utility cars by quantity
425        if (printHeader) {
426            printCarHeader(file, isManifest, !IS_TWO_COLUMN_TRACK);
427        }
428        for (Track track : tracks) {
429            if (trackNames.contains(track.getSplitName())) {
430                continue;
431            }
432            trackNames.add(track.getSplitName()); // use a track name once
433            // block car pick ups
434            for (RouteLocation rld : train.getTrainBlockingOrder()) {
435                for (int k = 0; k < carList.size(); k++) {
436                    Car car = carList.get(k);
437                    // block cars
438                    // caboose or FRED is placed at end of the train
439                    // passenger cars are already blocked in the car list
440                    // passenger cars with negative block numbers are placed at
441                    // the front of the train, positive numbers at the end of
442                    // the train.
443                    if (isNextCar(car, rl, rld)) {
444                        if (Setup.isSortByTrackNameEnabled() &&
445                                !track.getSplitName().equals(car.getSplitTrackName())) {
446                            continue;
447                        }
448                        _pickupCars = true;
449                        String s;
450                        if (car.isUtility()) {
451                            s = pickupUtilityCars(carList, car, isManifest, !IS_TWO_COLUMN_TRACK);
452                            if (s == null) {
453                                continue;
454                            }
455                            s = s.trim();
456                        } else {
457                            s = pickupCar(car, isManifest, !IS_TWO_COLUMN_TRACK).trim();
458                        }
459                        s = padAndTruncate(s, lineLength / 2);
460                        if (car.isLocalMove()) {
461                            s = formatColorString(s, Setup.getLocalColor());
462                            String sl = appendSetoutString(s, carList, car.getRouteDestination(), car, isManifest,
463                                    !IS_TWO_COLUMN_TRACK);
464                            // check for utility car, and local route with two
465                            // or more locations
466                            if (!sl.equals(s)) {
467                                s = sl;
468                                carList.remove(car); // done with this car, remove from list
469                                k--;
470                            } else {
471                                s = padAndTruncate(s + VERTICAL_LINE_CHAR, getLineLength(isManifest));
472                            }
473                        } else {
474                            s = formatColorString(s, Setup.getPickupColor());
475                            s = appendSetoutString(s, carList, rl, true, isManifest, !IS_TWO_COLUMN_TRACK);
476                        }
477                        addLine(file, s);
478                    }
479                }
480            }
481            if (!Setup.isSortByTrackNameEnabled()) {
482                break; // done
483            }
484        }
485        while (index < carList.size()) {
486            String s = padString("", lineLength / 2);
487            s = appendSetoutString(s, carList, rl, false, isManifest, !IS_TWO_COLUMN_TRACK);
488            String test = s.trim();
489            // null line contains |
490            if (test.length() > 1) {
491                addLine(file, s);
492            }
493        }
494    }
495
496    List<Car> doneCars = new ArrayList<>();
497
498    /**
499     * Produces a two column format for car pick ups and set outs. Sorted by
500     * track and then by destination. Track name in header format, track name
501     * removed from format. This routine is used to generate the "Two Column by
502     * Track" format.
503     *
504     * @param file        Manifest or switch list File
505     * @param train       The train
506     * @param carList     List of cars for this train
507     * @param rl          The RouteLocation being printed
508     * @param printHeader True if new location.
509     * @param isManifest  True if manifest, false if switch list.
510     */
511    protected void blockCarsByTrackNameTwoColumn(PrintWriter file, Train train, List<Car> carList, RouteLocation rl,
512            boolean printHeader, boolean isManifest) {
513        index = 0;
514        List<Track> tracks = rl.getLocation().getTracksByNameList(null);
515        List<String> trackNames = new ArrayList<>();
516        doneCars.clear();
517        clearUtilityCarTypes(); // list utility cars by quantity
518        if (printHeader) {
519            printCarHeader(file, isManifest, IS_TWO_COLUMN_TRACK);
520        }
521        for (Track track : tracks) {
522            String trackName = track.getSplitName();
523            if (trackNames.contains(trackName)) {
524                continue;
525            }
526            // block car pick ups
527            for (RouteLocation rld : train.getTrainBlockingOrder()) {
528                for (Car car : carList) {
529                    if (car.getTrack() != null &&
530                            car.getRouteLocation() == rl &&
531                            trackName.equals(car.getSplitTrackName()) &&
532                            ((car.getRouteDestination() == rld && !car.isCaboose() && !car.hasFred()) ||
533                                    (rld == train.getTrainTerminatesRouteLocation() &&
534                                            (car.isCaboose() || car.hasFred())))) {
535                        if (!trackNames.contains(trackName)) {
536                            printTrackNameHeader(file, trackName, isManifest);
537                        }
538                        trackNames.add(trackName); // use a track name once
539                        _pickupCars = true;
540                        String s;
541                        if (car.isUtility()) {
542                            s = pickupUtilityCars(carList, car, isManifest, IS_TWO_COLUMN_TRACK);
543                            if (s == null) {
544                                continue;
545                            }
546                            s = s.trim();
547                        } else {
548                            s = pickupCar(car, isManifest, IS_TWO_COLUMN_TRACK).trim();
549                        }
550                        s = padAndTruncate(s, getLineLength(isManifest) / 2);
551                        s = formatColorString(s, car.isLocalMove() ? Setup.getLocalColor() : Setup.getPickupColor());
552                        s = appendSetoutString(s, trackName, carList, rl, isManifest, IS_TWO_COLUMN_TRACK);
553                        addLine(file, s);
554                    }
555                }
556            }
557            for (Car car : carList) {
558                if (!doneCars.contains(car) &&
559                        car.getRouteDestination() == rl &&
560                        trackName.equals(car.getSplitDestinationTrackName())) {
561                    if (!trackNames.contains(trackName)) {
562                        printTrackNameHeader(file, trackName, isManifest);
563                    }
564                    trackNames.add(trackName); // use a track name once
565                    String s = padString("", getLineLength(isManifest) / 2);
566                    String so = appendSetoutString(s, carList, rl, car, isManifest, IS_TWO_COLUMN_TRACK);
567                    // check for utility car
568                    if (so.equals(s)) {
569                        continue;
570                    }
571                    String test = so.trim();
572                    if (test.length() > 1) // null line contains |
573                    {
574                        addLine(file, so);
575                    }
576                }
577            }
578        }
579    }
580
581    protected void printTrackComments(PrintWriter file, RouteLocation rl, List<Car> carList, boolean isManifest) {
582        Location location = rl.getLocation();
583        if (location != null) {
584            List<Track> tracks = location.getTracksByNameList(null);
585            for (Track track : tracks) {
586                if (isManifest && !track.isPrintManifestCommentEnabled() ||
587                        !isManifest && !track.isPrintSwitchListCommentEnabled()) {
588                    continue;
589                }
590                // any pick ups or set outs to this track?
591                boolean pickup = false;
592                boolean setout = false;
593                for (Car car : carList) {
594                    if (car.getRouteLocation() == rl && car.getTrack() != null && car.getTrack() == track) {
595                        pickup = true;
596                    }
597                    if (car.getRouteDestination() == rl &&
598                            car.getDestinationTrack() != null &&
599                            car.getDestinationTrack() == track) {
600                        setout = true;
601                    }
602                }
603                // print the appropriate comment if there's one
604                if (pickup && setout && !track.getCommentBothWithColor().equals(Track.NONE)) {
605                    newLine(file, track.getCommentBothWithColor(), isManifest);
606                } else if (pickup && !setout && !track.getCommentPickupWithColor().equals(Track.NONE)) {
607                    newLine(file, track.getCommentPickupWithColor(), isManifest);
608                } else if (!pickup && setout && !track.getCommentSetoutWithColor().equals(Track.NONE)) {
609                    newLine(file, track.getCommentSetoutWithColor(), isManifest);
610                }
611            }
612        }
613    }
614
615    protected void setPickupAndSetoutTimes(Train train, RouteLocation rl, List<RollingStock> list) {
616        String expectedDepartureTime = train.getExpectedDepartureTime(rl, true);
617        for (RollingStock rs : list) {
618            if (rs.getRouteLocation() == rl) {
619                rs.setPickupTime(expectedDepartureTime);
620            }
621            if (rs.getRouteDestination() == rl) {
622                rs.setSetoutTime(expectedDepartureTime);
623            }
624        }
625
626    }
627
628    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "SLF4J_FORMAT_SHOULD_BE_CONST",
629            justification = "Only when exception")
630    public static String getTrainMessage(Train train, RouteLocation rl) {
631        String expectedArrivalTime = train.getExpectedArrivalTime(rl);
632        String routeLocationName = rl.getSplitName();
633        String msg = "";
634        String messageFormatText = ""; // the text being formated in case there's an exception
635        try {
636            // Scheduled work at {0}
637            msg = MessageFormat.format(messageFormatText = TrainManifestText
638                    .getStringScheduledWork(),
639                    new Object[]{routeLocationName, train.getSplitName(),
640                            train.getDescription(), rl.getLocation().getDivisionName()});
641            if (train.isShowArrivalAndDepartureTimesEnabled()) {
642                if (rl == train.getTrainDepartsRouteLocation()) {
643                    // Scheduled work at {0}, departure time {1}
644                    msg = MessageFormat.format(messageFormatText = TrainManifestText
645                            .getStringWorkDepartureTime(),
646                            new Object[]{routeLocationName,
647                                    train.getFormatedDepartureTime(), train.getSplitName(),
648                                    train.getDescription(), rl.getLocation().getDivisionName()});
649                } else if (!rl.getDepartureTimeHourMinutes().equals(RouteLocation.NONE) &&
650                        rl != train.getTrainTerminatesRouteLocation()) {
651                    // Scheduled work at {0}, departure time {1}
652                    msg = MessageFormat.format(messageFormatText = TrainManifestText
653                            .getStringWorkDepartureTime(),
654                            new Object[]{routeLocationName, train.getExpectedDepartureTime(rl),
655                                    train.getSplitName(), train.getDescription(),
656                                    rl.getLocation().getDivisionName()});
657                } else if (Setup.isUseDepartureTimeEnabled() &&
658                        rl != train.getTrainTerminatesRouteLocation() &&
659                        !expectedArrivalTime.equals(Train.ALREADY_SERVICED)) {
660                    // Scheduled work at {0}, departure time {1}
661                    msg = MessageFormat.format(messageFormatText = TrainManifestText
662                            .getStringWorkDepartureTime(),
663                            new Object[]{routeLocationName,
664                                    train.getExpectedDepartureTime(rl), train.getSplitName(),
665                                    train.getDescription(), rl.getLocation().getDivisionName()});
666                } else if (!expectedArrivalTime.equals(Train.ALREADY_SERVICED)) {
667                    // Scheduled work at {0}, arrival time {1}
668                    msg = MessageFormat.format(messageFormatText = TrainManifestText
669                            .getStringWorkArrivalTime(),
670                            new Object[]{routeLocationName, expectedArrivalTime,
671                                    train.getSplitName(), train.getDescription(),
672                                    rl.getLocation().getDivisionName()});
673                }
674            }
675            return msg;
676        } catch (IllegalArgumentException e) {
677            msg = Bundle.getMessage("ErrorIllegalArgument",
678                    Bundle.getMessage("TitleManifestText"), e.getLocalizedMessage()) + NEW_LINE + messageFormatText;
679            log.error(msg);
680            log.error("Illegal argument", e);
681            return msg;
682        }
683    }
684
685    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "SLF4J_FORMAT_SHOULD_BE_CONST",
686            justification = "Only when exception")
687    public static String getSwitchListTrainStatus(Train train, RouteLocation rl) {
688        String expectedArrivalTime = train.getExpectedArrivalTime(rl);
689        String msg = "";
690        String messageFormatText = ""; // the text being formated in case there's an exception
691        try {
692            if (train.isLocalSwitcher()) {
693                // Use Manifest text for local departure
694                // Scheduled work at {0}, departure time {1}
695                msg = MessageFormat.format(messageFormatText = TrainManifestText.getStringWorkDepartureTime(),
696                        new Object[]{splitString(train.getTrainDepartsName()), train.getFormatedDepartureTime(),
697                                train.getSplitName(), train.getDescription(),
698                                rl.getLocation().getDivisionName()});
699            } else if (rl == train.getTrainDepartsRouteLocation()) {
700                // Departs {0} {1}bound at {2}
701                msg = MessageFormat.format(messageFormatText = TrainSwitchListText.getStringDepartsAt(),
702                        new Object[]{splitString(train.getTrainDepartsName()), rl.getTrainDirectionString(),
703                                train.getFormatedDepartureTime()});
704            } else if (Setup.isUseSwitchListDepartureTimeEnabled() &&
705                    rl != train.getTrainTerminatesRouteLocation() &&
706                    !train.isTrainEnRoute()) {
707                // Departs {0} at {1} expected arrival {2}, arrives {3}bound
708                msg = MessageFormat.format(
709                        messageFormatText = TrainSwitchListText.getStringDepartsAtExpectedArrival(),
710                        new Object[]{splitString(rl.getName()),
711                                train.getExpectedDepartureTime(rl), expectedArrivalTime,
712                                rl.getTrainDirectionString()});
713            } else if (Setup.isUseSwitchListDepartureTimeEnabled() &&
714                    rl == train.getCurrentRouteLocation() &&
715                    rl != train.getTrainTerminatesRouteLocation() &&
716                    !rl.getDepartureTimeHourMinutes().equals(RouteLocation.NONE)) {
717                // Departs {0} {1}bound at {2}
718                msg = MessageFormat.format(messageFormatText = TrainSwitchListText.getStringDepartsAt(),
719                        new Object[]{splitString(rl.getName()), rl.getTrainDirectionString(),
720                                rl.getFormatedDepartureTime()});
721            } else if (train.isTrainEnRoute()) {
722                if (!expectedArrivalTime.equals(Train.ALREADY_SERVICED)) {
723                    // Departed {0}, expect to arrive in {1}, arrives {2}bound
724                    msg = MessageFormat.format(messageFormatText = TrainSwitchListText.getStringDepartedExpected(),
725                            new Object[]{splitString(train.getTrainDepartsName()), expectedArrivalTime,
726                                    rl.getTrainDirectionString(), train.getCurrentLocationName()});
727                }
728            } else {
729                // Departs {0} at {1} expected arrival {2}, arrives {3}bound
730                msg = MessageFormat.format(
731                        messageFormatText = TrainSwitchListText.getStringDepartsAtExpectedArrival(),
732                        new Object[]{splitString(train.getTrainDepartsName()),
733                                train.getFormatedDepartureTime(), expectedArrivalTime,
734                                rl.getTrainDirectionString()});
735            }
736            return msg;
737        } catch (IllegalArgumentException e) {
738            msg = Bundle.getMessage("ErrorIllegalArgument",
739                    Bundle.getMessage("TitleSwitchListText"), e.getLocalizedMessage()) + NEW_LINE + messageFormatText;
740            log.error(msg);
741            log.error("Illegal argument", e);
742            return msg;
743        }
744    }
745
746    int index = 0;
747
748    /*
749     * Used by two column format. Local moves (pulls and spots) are lined up
750     * when using this format,
751     */
752    private String appendSetoutString(String s, List<Car> carList, RouteLocation rl, boolean local, boolean isManifest,
753            boolean isTwoColumnTrack) {
754        while (index < carList.size()) {
755            Car car = carList.get(index++);
756            if (local && car.isLocalMove()) {
757                continue; // skip local moves
758            }
759            // car list is already sorted by destination track
760            if (car.getRouteDestination() == rl) {
761                String so = appendSetoutString(s, carList, rl, car, isManifest, isTwoColumnTrack);
762                // check for utility car
763                if (!so.equals(s)) {
764                    return so;
765                }
766            }
767        }
768        // no set out for this line
769        return s + VERTICAL_LINE_CHAR + padAndTruncate("", getLineLength(isManifest) / 2 - 1);
770    }
771
772    /*
773     * Used by two column, track names shown in the columns.
774     */
775    private String appendSetoutString(String s, String trackName, List<Car> carList, RouteLocation rl,
776            boolean isManifest, boolean isTwoColumnTrack) {
777        for (Car car : carList) {
778            if (!doneCars.contains(car) &&
779                    car.getRouteDestination() == rl &&
780                    trackName.equals(car.getSplitDestinationTrackName())) {
781                doneCars.add(car);
782                String so = appendSetoutString(s, carList, rl, car, isManifest, isTwoColumnTrack);
783                // check for utility car
784                if (!so.equals(s)) {
785                    return so;
786                }
787            }
788        }
789        // no set out for this track
790        return s + VERTICAL_LINE_CHAR + padAndTruncate("", getLineLength(isManifest) / 2 - 1);
791    }
792
793    /*
794     * Appends to string the vertical line character, and the car set out
795     * string. Used in two column format.
796     */
797    private String appendSetoutString(String s, List<Car> carList, RouteLocation rl, Car car, boolean isManifest,
798            boolean isTwoColumnTrack) {
799        _dropCars = true;
800        String dropText;
801
802        if (car.isUtility()) {
803            dropText = setoutUtilityCars(carList, car, !LOCAL, isManifest, isTwoColumnTrack);
804            if (dropText == null) {
805                return s; // no changes to the input string
806            }
807        } else {
808            dropText = dropCar(car, isManifest, isTwoColumnTrack).trim();
809        }
810
811        dropText = padAndTruncate(dropText.trim(), getLineLength(isManifest) / 2 - 1);
812        dropText = formatColorString(dropText, car.isLocalMove() ? Setup.getLocalColor() : Setup.getDropColor());
813        return s + VERTICAL_LINE_CHAR + dropText;
814    }
815
816    /**
817     * Adds the car's pick up string to the output file using the truncated
818     * manifest format
819     *
820     * @param file       Manifest or switch list File
821     * @param car        The car being printed.
822     * @param isManifest True if manifest, false if switch list.
823     */
824    protected void pickUpCarTruncated(PrintWriter file, Car car, boolean isManifest) {
825        pickUpCar(file, car,
826                new StringBuffer(padAndTruncateIfNeeded(Setup.getPickupCarPrefix(), Setup.getManifestPrefixLength())),
827                Setup.getPickupTruncatedManifestMessageFormat(), isManifest);
828    }
829
830    /**
831     * Adds the car's pick up string to the output file using the manifest or
832     * switch list format
833     *
834     * @param file       Manifest or switch list File
835     * @param car        The car being printed.
836     * @param isManifest True if manifest, false if switch list.
837     */
838    protected void pickUpCar(PrintWriter file, Car car, boolean isManifest) {
839        if (isManifest) {
840            pickUpCar(file, car,
841                    new StringBuffer(
842                            padAndTruncateIfNeeded(Setup.getPickupCarPrefix(), Setup.getManifestPrefixLength())),
843                    Setup.getPickupManifestMessageFormat(), isManifest);
844        } else {
845            pickUpCar(file, car, new StringBuffer(
846                    padAndTruncateIfNeeded(Setup.getSwitchListPickupCarPrefix(), Setup.getSwitchListPrefixLength())),
847                    Setup.getPickupSwitchListMessageFormat(), isManifest);
848        }
849    }
850
851    private void pickUpCar(PrintWriter file, Car car, StringBuffer buf, String[] format, boolean isManifest) {
852        if (car.isLocalMove()) {
853            return; // print nothing local move, see dropCar
854        }
855        for (String attribute : format) {
856            String s = getCarAttribute(car, attribute, PICKUP, !LOCAL);
857            if (!checkStringLength(buf.toString() + s, isManifest)) {
858                addLine(file, buf, Setup.getPickupColor());
859                buf = new StringBuffer(TAB); // new line
860            }
861            buf.append(s);
862        }
863        addLine(file, buf, Setup.getPickupColor());
864    }
865
866    /**
867     * Returns the pick up car string. Useful for frames like train conductor
868     * and yardmaster.
869     *
870     * @param car              The car being printed.
871     * @param isManifest       when true use manifest format, when false use
872     *                         switch list format
873     * @param isTwoColumnTrack True if printing using two column format sorted
874     *                         by track name.
875     * @return pick up car string
876     */
877    public String pickupCar(Car car, boolean isManifest, boolean isTwoColumnTrack) {
878        StringBuffer buf = new StringBuffer();
879        String[] format;
880        if (isManifest && !isTwoColumnTrack) {
881            format = Setup.getPickupManifestMessageFormat();
882        } else if (!isManifest && !isTwoColumnTrack) {
883            format = Setup.getPickupSwitchListMessageFormat();
884        } else if (isManifest && isTwoColumnTrack) {
885            format = Setup.getPickupTwoColumnByTrackManifestMessageFormat();
886        } else {
887            format = Setup.getPickupTwoColumnByTrackSwitchListMessageFormat();
888        }
889        for (String attribute : format) {
890            buf.append(getCarAttribute(car, attribute, PICKUP, !LOCAL));
891        }
892        return buf.toString();
893    }
894
895    /**
896     * Adds the car's set out string to the output file using the truncated
897     * manifest format. Does not print out local moves. Local moves are only
898     * shown on the switch list for that location.
899     *
900     * @param file       Manifest or switch list File
901     * @param car        The car being printed.
902     * @param isManifest True if manifest, false if switch list.
903     */
904    protected void truncatedDropCar(PrintWriter file, Car car, boolean isManifest) {
905        // local move?
906        if (car.isLocalMove()) {
907            return; // yes, don't print local moves on train manifest
908        }
909        dropCar(file, car, new StringBuffer(Setup.getDropCarPrefix()), Setup.getDropTruncatedManifestMessageFormat(),
910                false, isManifest);
911    }
912
913    /**
914     * Adds the car's set out string to the output file using the manifest or
915     * switch list format
916     *
917     * @param file       Manifest or switch list File
918     * @param car        The car being printed.
919     * @param isManifest True if manifest, false if switch list.
920     */
921    protected void dropCar(PrintWriter file, Car car, boolean isManifest) {
922        boolean isLocal = car.isLocalMove();
923        if (isManifest) {
924            StringBuffer buf = new StringBuffer(
925                    padAndTruncateIfNeeded(Setup.getDropCarPrefix(), Setup.getManifestPrefixLength()));
926            String[] format = Setup.getDropManifestMessageFormat();
927            if (isLocal) {
928                buf = new StringBuffer(padAndTruncateIfNeeded(Setup.getLocalPrefix(), Setup.getManifestPrefixLength()));
929                format = Setup.getLocalManifestMessageFormat();
930            }
931            dropCar(file, car, buf, format, isLocal, isManifest);
932        } else {
933            StringBuffer buf = new StringBuffer(
934                    padAndTruncateIfNeeded(Setup.getSwitchListDropCarPrefix(), Setup.getSwitchListPrefixLength()));
935            String[] format = Setup.getDropSwitchListMessageFormat();
936            if (isLocal) {
937                buf = new StringBuffer(
938                        padAndTruncateIfNeeded(Setup.getSwitchListLocalPrefix(), Setup.getSwitchListPrefixLength()));
939                format = Setup.getLocalSwitchListMessageFormat();
940            }
941            dropCar(file, car, buf, format, isLocal, isManifest);
942        }
943    }
944
945    private void dropCar(PrintWriter file, Car car, StringBuffer buf, String[] format, boolean isLocal,
946            boolean isManifest) {
947        for (String attribute : format) {
948            String s = getCarAttribute(car, attribute, !PICKUP, isLocal);
949            if (!checkStringLength(buf.toString() + s, isManifest)) {
950                addLine(file, buf, isLocal ? Setup.getLocalColor() : Setup.getDropColor());
951                buf = new StringBuffer(TAB); // new line
952            }
953            buf.append(s);
954        }
955        addLine(file, buf, isLocal ? Setup.getLocalColor() : Setup.getDropColor());
956    }
957
958    /**
959     * Returns the drop car string. Useful for frames like train conductor and
960     * yardmaster.
961     *
962     * @param car              The car being printed.
963     * @param isManifest       when true use manifest format, when false use
964     *                         switch list format
965     * @param isTwoColumnTrack True if printing using two column format.
966     * @return drop car string
967     */
968    public String dropCar(Car car, boolean isManifest, boolean isTwoColumnTrack) {
969        StringBuffer buf = new StringBuffer();
970        String[] format;
971        if (isManifest && !isTwoColumnTrack) {
972            format = Setup.getDropManifestMessageFormat();
973        } else if (!isManifest && !isTwoColumnTrack) {
974            format = Setup.getDropSwitchListMessageFormat();
975        } else if (isManifest && isTwoColumnTrack) {
976            format = Setup.getDropTwoColumnByTrackManifestMessageFormat();
977        } else {
978            format = Setup.getDropTwoColumnByTrackSwitchListMessageFormat();
979        }
980        // TODO the Setup.Location doesn't work correctly for the conductor
981        // window due to the fact that the car can be in the train and not
982        // at its starting location.
983        // Therefore we use the local true to disable it.
984        boolean local = false;
985        if (car.getTrack() == null) {
986            local = true;
987        }
988        for (String attribute : format) {
989            buf.append(getCarAttribute(car, attribute, !PICKUP, local));
990        }
991        return buf.toString();
992    }
993
994    /**
995     * Returns the move car string. Useful for frames like train conductor and
996     * yardmaster.
997     *
998     * @param car        The car being printed.
999     * @param isManifest when true use manifest format, when false use switch
1000     *                   list format
1001     * @return move car string
1002     */
1003    public String localMoveCar(Car car, boolean isManifest) {
1004        StringBuffer buf = new StringBuffer();
1005        String[] format;
1006        if (isManifest) {
1007            format = Setup.getLocalManifestMessageFormat();
1008        } else {
1009            format = Setup.getLocalSwitchListMessageFormat();
1010        }
1011        for (String attribute : format) {
1012            buf.append(getCarAttribute(car, attribute, !PICKUP, LOCAL));
1013        }
1014        return buf.toString();
1015    }
1016
1017    List<String> utilityCarTypes = new ArrayList<>();
1018    private static final int UTILITY_CAR_COUNT_FIELD_SIZE = 3;
1019
1020    /**
1021     * Add a list of utility cars scheduled for pick up from the route location
1022     * to the output file. The cars are blocked by destination.
1023     *
1024     * @param file       Manifest or Switch List File.
1025     * @param carList    List of cars for this train.
1026     * @param car        The utility car.
1027     * @param isTruncate True if manifest is to be truncated
1028     * @param isManifest True if manifest, false if switch list.
1029     */
1030    protected void pickupUtilityCars(PrintWriter file, List<Car> carList, Car car, boolean isTruncate,
1031            boolean isManifest) {
1032        // list utility cars by type, track, length, and load
1033        String[] format;
1034        if (isManifest) {
1035            format = Setup.getPickupUtilityManifestMessageFormat();
1036        } else {
1037            format = Setup.getPickupUtilitySwitchListMessageFormat();
1038        }
1039        if (isTruncate && isManifest) {
1040            format = Setup.createTruncatedManifestMessageFormat(format);
1041        }
1042        int count = countUtilityCars(format, carList, car, PICKUP);
1043        if (count == 0) {
1044            return; // already printed out this car type
1045        }
1046        pickUpCar(file, car,
1047                new StringBuffer(padAndTruncateIfNeeded(Setup.getPickupCarPrefix(),
1048                        isManifest ? Setup.getManifestPrefixLength() : Setup.getSwitchListPrefixLength()) +
1049                        SPACE +
1050                        padString(Integer.toString(count), UTILITY_CAR_COUNT_FIELD_SIZE)),
1051                format, isManifest);
1052    }
1053
1054    /**
1055     * Add a list of utility cars scheduled for drop at the route location to
1056     * the output file.
1057     *
1058     * @param file       Manifest or Switch List File.
1059     * @param carList    List of cars for this train.
1060     * @param car        The utility car.
1061     * @param isTruncate True if manifest is to be truncated
1062     * @param isManifest True if manifest, false if switch list.
1063     */
1064    protected void setoutUtilityCars(PrintWriter file, List<Car> carList, Car car, boolean isTruncate,
1065            boolean isManifest) {
1066        boolean isLocal = car.isLocalMove();
1067        StringBuffer buf;
1068        String[] format;
1069        if (isLocal && isManifest) {
1070            buf = new StringBuffer(padAndTruncateIfNeeded(Setup.getLocalPrefix(), Setup.getManifestPrefixLength()));
1071            format = Setup.getLocalUtilityManifestMessageFormat();
1072        } else if (!isLocal && isManifest) {
1073            buf = new StringBuffer(padAndTruncateIfNeeded(Setup.getDropCarPrefix(), Setup.getManifestPrefixLength()));
1074            format = Setup.getDropUtilityManifestMessageFormat();
1075        } else if (isLocal && !isManifest) {
1076            buf = new StringBuffer(
1077                    padAndTruncateIfNeeded(Setup.getSwitchListLocalPrefix(), Setup.getSwitchListPrefixLength()));
1078            format = Setup.getLocalUtilitySwitchListMessageFormat();
1079        } else {
1080            buf = new StringBuffer(
1081                    padAndTruncateIfNeeded(Setup.getSwitchListDropCarPrefix(), Setup.getSwitchListPrefixLength()));
1082            format = Setup.getDropUtilitySwitchListMessageFormat();
1083        }
1084        if (isTruncate && isManifest) {
1085            format = Setup.createTruncatedManifestMessageFormat(format);
1086        }
1087
1088        int count = countUtilityCars(format, carList, car, !PICKUP);
1089        if (count == 0) {
1090            return; // already printed out this car type
1091        }
1092        buf.append(SPACE + padString(Integer.toString(count), UTILITY_CAR_COUNT_FIELD_SIZE));
1093        dropCar(file, car, buf, format, isLocal, isManifest);
1094    }
1095
1096    public String pickupUtilityCars(List<Car> carList, Car car, boolean isManifest, boolean isTwoColumnTrack) {
1097        int count = countPickupUtilityCars(carList, car, isManifest);
1098        if (count == 0) {
1099            return null;
1100        }
1101        String[] format;
1102        if (isManifest && !isTwoColumnTrack) {
1103            format = Setup.getPickupUtilityManifestMessageFormat();
1104        } else if (!isManifest && !isTwoColumnTrack) {
1105            format = Setup.getPickupUtilitySwitchListMessageFormat();
1106        } else if (isManifest && isTwoColumnTrack) {
1107            format = Setup.getPickupTwoColumnByTrackUtilityManifestMessageFormat();
1108        } else {
1109            format = Setup.getPickupTwoColumnByTrackUtilitySwitchListMessageFormat();
1110        }
1111        StringBuffer buf = new StringBuffer(SPACE + padString(Integer.toString(count), UTILITY_CAR_COUNT_FIELD_SIZE));
1112        for (String attribute : format) {
1113            buf.append(getCarAttribute(car, attribute, PICKUP, !LOCAL));
1114        }
1115        return buf.toString();
1116    }
1117
1118    public int countPickupUtilityCars(List<Car> carList, Car car, boolean isManifest) {
1119        // list utility cars by type, track, length, and load
1120        String[] format;
1121        if (isManifest) {
1122            format = Setup.getPickupUtilityManifestMessageFormat();
1123        } else {
1124            format = Setup.getPickupUtilitySwitchListMessageFormat();
1125        }
1126        return countUtilityCars(format, carList, car, PICKUP);
1127    }
1128
1129    /**
1130     * For the Conductor and Yardmaster windows.
1131     *
1132     * @param carList    List of cars for this train.
1133     * @param car        The utility car.
1134     * @param isLocal    True if local move.
1135     * @param isManifest True if manifest, false if switch list.
1136     * @return A string representing the work of identical utility cars.
1137     */
1138    public String setoutUtilityCars(List<Car> carList, Car car, boolean isLocal, boolean isManifest) {
1139        return setoutUtilityCars(carList, car, isLocal, isManifest, !IS_TWO_COLUMN_TRACK);
1140    }
1141
1142    protected String setoutUtilityCars(List<Car> carList, Car car, boolean isLocal, boolean isManifest,
1143            boolean isTwoColumnTrack) {
1144        int count = countSetoutUtilityCars(carList, car, isLocal, isManifest);
1145        if (count == 0) {
1146            return null;
1147        }
1148        // list utility cars by type, track, length, and load
1149        String[] format;
1150        if (isLocal && isManifest && !isTwoColumnTrack) {
1151            format = Setup.getLocalUtilityManifestMessageFormat();
1152        } else if (isLocal && !isManifest && !isTwoColumnTrack) {
1153            format = Setup.getLocalUtilitySwitchListMessageFormat();
1154        } else if (!isLocal && !isManifest && !isTwoColumnTrack) {
1155            format = Setup.getDropUtilitySwitchListMessageFormat();
1156        } else if (!isLocal && isManifest && !isTwoColumnTrack) {
1157            format = Setup.getDropUtilityManifestMessageFormat();
1158        } else if (isManifest && isTwoColumnTrack) {
1159            format = Setup.getDropTwoColumnByTrackUtilityManifestMessageFormat();
1160        } else {
1161            format = Setup.getDropTwoColumnByTrackUtilitySwitchListMessageFormat();
1162        }
1163        StringBuffer buf = new StringBuffer(SPACE + padString(Integer.toString(count), UTILITY_CAR_COUNT_FIELD_SIZE));
1164        // TODO the Setup.Location doesn't work correctly for the conductor
1165        // window due to the fact that the car can be in the train and not
1166        // at its starting location.
1167        // Therefore we use the local true to disable it.
1168        if (car.getTrack() == null) {
1169            isLocal = true;
1170        }
1171        for (String attribute : format) {
1172            buf.append(getCarAttribute(car, attribute, !PICKUP, isLocal));
1173        }
1174        return buf.toString();
1175    }
1176
1177    public int countSetoutUtilityCars(List<Car> carList, Car car, boolean isLocal, boolean isManifest) {
1178        // list utility cars by type, track, length, and load
1179        String[] format;
1180        if (isLocal && isManifest) {
1181            format = Setup.getLocalUtilityManifestMessageFormat();
1182        } else if (isLocal && !isManifest) {
1183            format = Setup.getLocalUtilitySwitchListMessageFormat();
1184        } else if (!isLocal && !isManifest) {
1185            format = Setup.getDropUtilitySwitchListMessageFormat();
1186        } else {
1187            format = Setup.getDropUtilityManifestMessageFormat();
1188        }
1189        return countUtilityCars(format, carList, car, !PICKUP);
1190    }
1191
1192    /**
1193     * Scans the car list for utility cars that have the same attributes as the
1194     * car provided. Returns 0 if this car type has already been processed,
1195     * otherwise the number of cars with the same attribute.
1196     *
1197     * @param format   Message format.
1198     * @param carList  List of cars for this train
1199     * @param car      The utility car.
1200     * @param isPickup True if pick up, false if set out.
1201     * @return 0 if the car type has already been processed
1202     */
1203    protected int countUtilityCars(String[] format, List<Car> carList, Car car, boolean isPickup) {
1204        int count = 0;
1205        // figure out if the user wants to show the car's length
1206        boolean showLength = showUtilityCarLength(format);
1207        // figure out if the user want to show the car's loads
1208        boolean showLoad = showUtilityCarLoad(format);
1209        boolean showLocation = false;
1210        boolean showDestination = false;
1211        String carType = car.getTypeName().split(HYPHEN)[0];
1212        String carAttributes;
1213        // Note for car pick up: type, id, track name. For set out type, track
1214        // name, id (reversed).
1215        if (isPickup) {
1216            carAttributes = carType + car.getRouteLocationId() + car.getSplitTrackName();
1217            showDestination = showUtilityCarDestination(format);
1218            if (showDestination) {
1219                carAttributes = carAttributes + car.getRouteDestinationId();
1220            }
1221        } else {
1222            // set outs and local moves
1223            carAttributes = carType + car.getSplitDestinationTrackName() + car.getRouteDestinationId();
1224            showLocation = showUtilityCarLocation(format);
1225            if (showLocation && car.getTrack() != null) {
1226                carAttributes = carAttributes + car.getRouteLocationId();
1227            }
1228        }
1229        if (car.isLocalMove()) {
1230            carAttributes = carAttributes + car.getSplitTrackName();
1231        }
1232        if (showLength) {
1233            carAttributes = carAttributes + car.getLength();
1234        }
1235        if (showLoad) {
1236            carAttributes = carAttributes + car.getLoadName();
1237        }
1238        // have we already done this car type?
1239        if (!utilityCarTypes.contains(carAttributes)) {
1240            utilityCarTypes.add(carAttributes); // don't do this type again
1241            // determine how many cars of this type
1242            for (Car c : carList) {
1243                if (!c.isUtility()) {
1244                    continue;
1245                }
1246                String cType = c.getTypeName().split(HYPHEN)[0];
1247                if (!cType.equals(carType)) {
1248                    continue;
1249                }
1250                if (showLength && !c.getLength().equals(car.getLength())) {
1251                    continue;
1252                }
1253                if (showLoad && !c.getLoadName().equals(car.getLoadName())) {
1254                    continue;
1255                }
1256                if (showLocation && !c.getRouteLocationId().equals(car.getRouteLocationId())) {
1257                    continue;
1258                }
1259                if (showDestination && !c.getRouteDestinationId().equals(car.getRouteDestinationId())) {
1260                    continue;
1261                }
1262                if (car.isLocalMove() ^ c.isLocalMove()) {
1263                    continue;
1264                }
1265                if (isPickup &&
1266                        c.getRouteLocation() == car.getRouteLocation() &&
1267                        c.getSplitTrackName().equals(car.getSplitTrackName())) {
1268                    count++;
1269                }
1270                if (!isPickup &&
1271                        c.getRouteDestination() == car.getRouteDestination() &&
1272                        c.getSplitDestinationTrackName().equals(car.getSplitDestinationTrackName()) &&
1273                        (c.getSplitTrackName().equals(car.getSplitTrackName()) || !c.isLocalMove())) {
1274                    count++;
1275                }
1276            }
1277        }
1278        return count;
1279    }
1280
1281    public void clearUtilityCarTypes() {
1282        utilityCarTypes.clear();
1283    }
1284
1285    private boolean showUtilityCarLength(String[] mFormat) {
1286        return showUtilityCarAttribute(Setup.LENGTH, mFormat);
1287    }
1288
1289    private boolean showUtilityCarLoad(String[] mFormat) {
1290        return showUtilityCarAttribute(Setup.LOAD, mFormat);
1291    }
1292
1293    private boolean showUtilityCarLocation(String[] mFormat) {
1294        return showUtilityCarAttribute(Setup.LOCATION, mFormat);
1295    }
1296
1297    private boolean showUtilityCarDestination(String[] mFormat) {
1298        return showUtilityCarAttribute(Setup.DESTINATION, mFormat) ||
1299                showUtilityCarAttribute(Setup.DEST_TRACK, mFormat);
1300    }
1301
1302    private boolean showUtilityCarAttribute(String string, String[] mFormat) {
1303        for (String s : mFormat) {
1304            if (s.equals(string)) {
1305                return true;
1306            }
1307        }
1308        return false;
1309    }
1310
1311    /**
1312     * Writes a line to the build report file
1313     *
1314     * @param file   build report file
1315     * @param level  print level
1316     * @param string string to write
1317     */
1318    public static void addLine(PrintWriter file, String level, String string) {
1319        log.debug("addLine: {}", string);
1320        if (file != null) {
1321            String[] lines = string.split(NEW_LINE);
1322            for (String line : lines) {
1323                printLine(file, level, line);
1324            }
1325        }
1326    }
1327
1328    // only used by build report
1329    private static void printLine(PrintWriter file, String level, String string) {
1330        int lineLengthMax = getLineLength(Setup.PORTRAIT, Setup.MONOSPACED, Font.PLAIN, Setup.getBuildReportFontSize());
1331        if (string.length() > lineLengthMax) {
1332            String[] words = string.split(SPACE);
1333            StringBuffer sb = new StringBuffer();
1334            for (String word : words) {
1335                if (sb.length() + word.length() < lineLengthMax) {
1336                    sb.append(word + SPACE);
1337                } else {
1338                    file.println(level + BUILD_REPORT_CHAR + SPACE + sb.toString());
1339                    sb = new StringBuffer(word + SPACE);
1340                }
1341            }
1342            string = sb.toString();
1343        }
1344        file.println(level + BUILD_REPORT_CHAR + SPACE + string);
1345    }
1346
1347    /**
1348     * Writes string to file. No line length wrap or protection.
1349     *
1350     * @param file   The File to write to.
1351     * @param string The string to write.
1352     */
1353    protected void addLine(PrintWriter file, String string) {
1354        log.debug("addLine: {}", string);
1355        if (file != null) {
1356            file.println(string);
1357        }
1358    }
1359
1360    /**
1361     * Writes a string to a file. Checks for string length, and will
1362     * automatically wrap lines.
1363     *
1364     * @param file       The File to write to.
1365     * @param string     The string to write.
1366     * @param isManifest set true for manifest page orientation, false for
1367     *                   switch list orientation
1368     */
1369    protected void newLine(PrintWriter file, String string, boolean isManifest) {
1370        String[] lines = string.split(NEW_LINE);
1371        for (String line : lines) {
1372            String[] words = line.split(SPACE);
1373            StringBuffer sb = new StringBuffer();
1374            for (String word : words) {
1375                if (checkStringLength(sb.toString() + word, isManifest)) {
1376                    sb.append(word + SPACE);
1377                } else {
1378                    if (sb.length() > 0) {
1379                        sb.setLength(sb.length() - 1); // remove last space added to string
1380                        addLine(file, sb.toString());
1381                    }
1382                    sb = new StringBuffer(word + SPACE);
1383                }
1384            }
1385            if (sb.length() > 0) {
1386                sb.setLength(sb.length() - 1); // remove last space added to string
1387            }
1388            addLine(file, sb.toString());
1389        }
1390    }
1391
1392    /**
1393     * Adds a blank line to the file.
1394     *
1395     * @param file The File to write to.
1396     */
1397    protected void newLine(PrintWriter file) {
1398        file.println(BLANK_LINE);
1399    }
1400
1401    /**
1402     * Splits a string (example-number) as long as the second part of the string
1403     * is an integer or if the first character after the hyphen is a left
1404     * parenthesis "(".
1405     *
1406     * @param name The string to split if necessary.
1407     * @return First half of the string.
1408     */
1409    public static String splitString(String name) {
1410        String[] splitname = name.split(HYPHEN);
1411        // is the hyphen followed by a number or left parenthesis?
1412        if (splitname.length > 1 && !splitname[1].startsWith("(")) {
1413            try {
1414                Integer.parseInt(splitname[1]);
1415            } catch (NumberFormatException e) {
1416                // no return full name
1417                return name.trim();
1418            }
1419        }
1420        return splitname[0].trim();
1421    }
1422
1423    /**
1424     * Splits a string if there's a hyphen followed by a left parenthesis "-(".
1425     * 
1426     * @param name the string to split
1427     * @return First half of the string.
1428     */
1429    public static String splitStringLeftParenthesis(String name) {
1430        String[] splitname = name.split(HYPHEN);
1431        if (splitname.length > 1 && splitname[1].startsWith("(")) {
1432            return splitname[0].trim();
1433        }
1434        return name.trim();
1435    }
1436
1437    // returns true if there's work at location
1438    protected boolean isThereWorkAtLocation(List<Car> carList, List<Engine> engList, RouteLocation rl) {
1439        if (carList != null) {
1440            for (Car car : carList) {
1441                if (car.getRouteLocation() == rl || car.getRouteDestination() == rl) {
1442                    return true;
1443                }
1444            }
1445        }
1446        if (engList != null) {
1447            for (Engine eng : engList) {
1448                if (eng.getRouteLocation() == rl || eng.getRouteDestination() == rl) {
1449                    return true;
1450                }
1451            }
1452        }
1453        return false;
1454    }
1455
1456    /**
1457     * returns true if the train has work at the location
1458     *
1459     * @param train    The Train.
1460     * @param location The Location.
1461     * @return true if the train has work at the location
1462     */
1463    public static boolean isThereWorkAtLocation(Train train, Location location) {
1464        if (isThereWorkAtLocation(train, location, InstanceManager.getDefault(CarManager.class).getList(train))) {
1465            return true;
1466        }
1467        if (isThereWorkAtLocation(train, location, InstanceManager.getDefault(EngineManager.class).getList(train))) {
1468            return true;
1469        }
1470        return false;
1471    }
1472
1473    private static boolean isThereWorkAtLocation(Train train, Location location, List<? extends RollingStock> list) {
1474        for (RollingStock rs : list) {
1475            if ((rs.getRouteLocation() != null &&
1476                    rs.getTrack() != null &&
1477                    rs.getRouteLocation().getSplitName()
1478                            .equals(location.getSplitName())) ||
1479                    (rs.getRouteDestination() != null &&
1480                            rs.getRouteDestination().getSplitName().equals(location.getSplitName()))) {
1481                return true;
1482            }
1483        }
1484        return false;
1485    }
1486
1487    protected void addCarsLocationUnknown(PrintWriter file, boolean isManifest) {
1488        List<Car> cars = carManager.getCarsLocationUnknown();
1489        if (cars.size() == 0) {
1490            return; // no cars to search for!
1491        }
1492        newLine(file);
1493        newLine(file, Setup.getMiaComment(), isManifest);
1494        if (Setup.isPrintHeadersEnabled()) {
1495            printHorizontalLine1(file, isManifest);
1496            newLine(file, SPACE + getHeader(Setup.getMissingCarMessageFormat(), false, false, false), isManifest);
1497            printHorizontalLine2(file, isManifest);
1498        }
1499        for (Car car : cars) {
1500            addSearchForCar(file, car, isManifest);
1501        }
1502    }
1503
1504    private void addSearchForCar(PrintWriter file, Car car, boolean isManifest) {
1505        StringBuffer buf = new StringBuffer();
1506        String[] format = Setup.getMissingCarMessageFormat();
1507        for (String attribute : format) {
1508            buf.append(getCarAttribute(car, attribute, false, false));
1509        }
1510        newLine(file, buf.toString(), isManifest);
1511    }
1512
1513    /*
1514     * Gets an engine's attribute String. Returns empty if there isn't an
1515     * attribute and not using the tabular feature. isPickup true when engine is
1516     * being picked up.
1517     */
1518    private String getEngineAttribute(Engine engine, String attribute, boolean isPickup) {
1519        if (!attribute.equals(Setup.BLANK)) {
1520            String s = SPACE + getEngineAttrib(engine, attribute, isPickup);
1521            if (Setup.isTabEnabled() || !s.isBlank()) {
1522                return s;
1523            }
1524        }
1525        return "";
1526    }
1527
1528    /*
1529     * Can not use String case statement since Setup.MODEL, etc, are not fixed
1530     * strings.
1531     */
1532    private String getEngineAttrib(Engine engine, String attribute, boolean isPickup) {
1533        if (attribute.equals(Setup.MODEL)) {
1534            return padAndTruncateIfNeeded(splitStringLeftParenthesis(engine.getModel()),
1535                    InstanceManager.getDefault(EngineModels.class).getMaxNameLength());
1536        } else if (attribute.equals(Setup.HP)) {
1537            return padAndTruncateIfNeeded(engine.getHp(), 5) +
1538                    (Setup.isPrintHeadersEnabled() ? "" : TrainManifestHeaderText.getStringHeader_Hp());
1539        } else if (attribute.equals(Setup.CONSIST)) {
1540            return padAndTruncateIfNeeded(engine.getConsistName(),
1541                    InstanceManager.getDefault(ConsistManager.class).getMaxNameLength());
1542        } else if (attribute.equals(Setup.DCC_ADDRESS)) {
1543            return padAndTruncateIfNeeded(engine.getDccAddress(),
1544                    TrainManifestHeaderText.getStringHeader_DCC_Address().length());
1545        } else if (attribute.equals(Setup.COMMENT)) {
1546            return padAndTruncateIfNeeded(engine.getComment(), engineManager.getMaxCommentLength());
1547        }
1548        return getRollingStockAttribute(engine, attribute, isPickup, false);
1549    }
1550
1551    /*
1552     * Gets a car's attribute String. Returns empty if there isn't an attribute
1553     * and not using the tabular feature. isPickup true when car is being picked
1554     * up. isLocal true when car is performing a local move.
1555     */
1556    private String getCarAttribute(Car car, String attribute, boolean isPickup, boolean isLocal) {
1557        if (!attribute.equals(Setup.BLANK)) {
1558            String s = SPACE + getCarAttrib(car, attribute, isPickup, isLocal);
1559            if (Setup.isTabEnabled() || !s.isBlank()) {
1560                return s;
1561            }
1562        }
1563        return "";
1564    }
1565
1566    private String getCarAttrib(Car car, String attribute, boolean isPickup, boolean isLocal) {
1567        if (attribute.equals(Setup.LOAD)) {
1568            return ((car.isCaboose() && !Setup.isPrintCabooseLoadEnabled()) ||
1569                    (car.isPassenger() && !Setup.isPrintPassengerLoadEnabled()))
1570                            ? padAndTruncateIfNeeded("",
1571                                    InstanceManager.getDefault(CarLoads.class).getMaxNameLength())
1572                            : padAndTruncateIfNeeded(car.getLoadName().split(HYPHEN)[0],
1573                                    InstanceManager.getDefault(CarLoads.class).getMaxNameLength());
1574        } else if (attribute.equals(Setup.LOAD_TYPE)) {
1575            return padAndTruncateIfNeeded(car.getLoadType(),
1576                    TrainManifestHeaderText.getStringHeader_Load_Type().length());
1577        } else if (attribute.equals(Setup.HAZARDOUS)) {
1578            return (car.isHazardous() ? Setup.getHazardousMsg()
1579                    : padAndTruncateIfNeeded("", Setup.getHazardousMsg().length()));
1580        } else if (attribute.equals(Setup.DROP_COMMENT)) {
1581            return padAndTruncateIfNeeded(car.getDropComment(),
1582                    InstanceManager.getDefault(CarLoads.class).getMaxLoadCommentLength());
1583        } else if (attribute.equals(Setup.PICKUP_COMMENT)) {
1584            return padAndTruncateIfNeeded(car.getPickupComment(),
1585                    InstanceManager.getDefault(CarLoads.class).getMaxLoadCommentLength());
1586        } else if (attribute.equals(Setup.KERNEL)) {
1587            return padAndTruncateIfNeeded(car.getKernelName(),
1588                    InstanceManager.getDefault(KernelManager.class).getMaxNameLength());
1589        } else if (attribute.equals(Setup.KERNEL_SIZE)) {
1590            if (car.isLead()) {
1591                return padAndTruncateIfNeeded(Integer.toString(car.getKernel().getSize()), 2);
1592            }
1593            return SPACE + SPACE; // assumes that kernel size is 99 or less
1594        } else if (attribute.equals(Setup.RWE)) {
1595            if (!car.getReturnWhenEmptyDestinationName().equals(Car.NONE)) {
1596                // format RWE destination and track name
1597                String rweAndTrackName = car.getSplitReturnWhenEmptyDestinationName();
1598                if (!car.getReturnWhenEmptyDestTrackName().equals(Car.NONE)) {
1599                    rweAndTrackName = rweAndTrackName + "," + SPACE + car.getSplitReturnWhenEmptyDestinationTrackName();
1600                }
1601                return Setup.isPrintHeadersEnabled()
1602                        ? padAndTruncateIfNeeded(rweAndTrackName, locationManager.getMaxLocationAndTrackNameLength())
1603                        : padAndTruncateIfNeeded(
1604                                TrainManifestHeaderText.getStringHeader_RWE() + SPACE + rweAndTrackName,
1605                                locationManager.getMaxLocationAndTrackNameLength() +
1606                                        TrainManifestHeaderText.getStringHeader_RWE().length() +
1607                                        3);
1608            }
1609            return padAndTruncateIfNeeded("", locationManager.getMaxLocationAndTrackNameLength());
1610        } else if (attribute.equals(Setup.FINAL_DEST)) {
1611            return Setup.isPrintHeadersEnabled()
1612                    ? padAndTruncateIfNeeded(car.getSplitFinalDestinationName(),
1613                            locationManager.getMaxLocationNameLength())
1614                    : padAndTruncateIfNeeded(
1615                            TrainManifestText.getStringFinalDestination() +
1616                                    SPACE +
1617                                    car.getSplitFinalDestinationName(),
1618                            locationManager.getMaxLocationNameLength() +
1619                                    TrainManifestText.getStringFinalDestination().length() +
1620                                    1);
1621        } else if (attribute.equals(Setup.FINAL_DEST_TRACK)) {
1622            // format final destination and track name
1623            String FDAndTrackName = car.getSplitFinalDestinationName();
1624            if (!car.getFinalDestinationTrackName().equals(Car.NONE)) {
1625                FDAndTrackName = FDAndTrackName + "," + SPACE + car.getSplitFinalDestinationTrackName();
1626            }
1627            return Setup.isPrintHeadersEnabled()
1628                    ? padAndTruncateIfNeeded(FDAndTrackName, locationManager.getMaxLocationAndTrackNameLength() + 2)
1629                    : padAndTruncateIfNeeded(TrainManifestText.getStringFinalDestination() + SPACE + FDAndTrackName,
1630                            locationManager.getMaxLocationAndTrackNameLength() +
1631                                    TrainManifestText.getStringFinalDestination().length() +
1632                                    3);
1633        } else if (attribute.equals(Setup.DIVISION)) {
1634            return padAndTruncateIfNeeded(car.getDivisionName(),
1635                    InstanceManager.getDefault(DivisionManager.class).getMaxDivisionNameLength());
1636        } else if (attribute.equals(Setup.BLOCKING_ORDER)) {
1637            if (car.isPassenger()) {
1638                return padAndTruncateIfNeeded(Integer.toString(car.getBlocking()), 3);
1639            }
1640            return SPACE + SPACE + SPACE; // assumes that blocking order is +/- 99
1641        } else if (attribute.equals(Setup.COMMENT)) {
1642            return padAndTruncateIfNeeded(car.getComment(), carManager.getMaxCommentLength());
1643        }
1644        return getRollingStockAttribute(car, attribute, isPickup, isLocal);
1645    }
1646
1647    private String getRollingStockAttribute(RollingStock rs, String attribute, boolean isPickup, boolean isLocal) {
1648        try {
1649            if (attribute.equals(Setup.NUMBER)) {
1650                return padAndTruncateIfNeeded(splitString(rs.getNumber()), Control.max_len_string_print_road_number);
1651            } else if (attribute.equals(Setup.ROAD)) {
1652                String road = rs.getRoadName().split(HYPHEN)[0];
1653                return padAndTruncateIfNeeded(road, InstanceManager.getDefault(CarRoads.class).getMaxNameLength());
1654            } else if (attribute.equals(Setup.TYPE)) {
1655                String type = rs.getTypeName().split(HYPHEN)[0];
1656                return padAndTruncateIfNeeded(type, InstanceManager.getDefault(CarTypes.class).getMaxNameLength());
1657            } else if (attribute.equals(Setup.LENGTH)) {
1658                return padAndTruncateIfNeeded(rs.getLength() + Setup.getLengthUnitAbv(),
1659                        InstanceManager.getDefault(CarLengths.class).getMaxNameLength());
1660            } else if (attribute.equals(Setup.WEIGHT)) {
1661                return padAndTruncateIfNeeded(Integer.toString(rs.getAdjustedWeightTons()),
1662                        Control.max_len_string_weight_name) +
1663                        (Setup.isPrintHeadersEnabled() ? "" : TrainManifestHeaderText.getStringHeader_Weight());
1664            } else if (attribute.equals(Setup.COLOR)) {
1665                return padAndTruncateIfNeeded(rs.getColor(),
1666                        InstanceManager.getDefault(CarColors.class).getMaxNameLength());
1667            } else if (((attribute.equals(Setup.LOCATION)) && (isPickup || isLocal)) ||
1668                    (attribute.equals(Setup.TRACK) && isPickup)) {
1669                return Setup.isPrintHeadersEnabled()
1670                        ? padAndTruncateIfNeeded(rs.getSplitTrackName(),
1671                                locationManager.getMaxTrackNameLength())
1672                        : padAndTruncateIfNeeded(
1673                                TrainManifestText.getStringFrom() + SPACE + rs.getSplitTrackName(),
1674                                TrainManifestText.getStringFrom().length() +
1675                                        locationManager.getMaxTrackNameLength() +
1676                                        1);
1677            } else if (attribute.equals(Setup.LOCATION) && !isPickup && !isLocal) {
1678                return Setup.isPrintHeadersEnabled()
1679                        ? padAndTruncateIfNeeded(rs.getSplitLocationName(),
1680                                locationManager.getMaxLocationNameLength())
1681                        : padAndTruncateIfNeeded(
1682                                TrainManifestText.getStringFrom() + SPACE + rs.getSplitLocationName(),
1683                                locationManager.getMaxLocationNameLength() +
1684                                        TrainManifestText.getStringFrom().length() +
1685                                        1);
1686            } else if (attribute.equals(Setup.DESTINATION) && isPickup) {
1687                if (Setup.isPrintHeadersEnabled()) {
1688                    return padAndTruncateIfNeeded(rs.getSplitDestinationName(),
1689                            locationManager.getMaxLocationNameLength());
1690                }
1691                if (Setup.isTabEnabled()) {
1692                    return padAndTruncateIfNeeded(
1693                            TrainManifestText.getStringDest() + SPACE + rs.getSplitDestinationName(),
1694                            TrainManifestText.getStringDest().length() +
1695                                    locationManager.getMaxLocationNameLength() +
1696                                    1);
1697                } else {
1698                    return TrainManifestText.getStringDestination() +
1699                            SPACE +
1700                            rs.getSplitDestinationName();
1701                }
1702            } else if ((attribute.equals(Setup.DESTINATION) || attribute.equals(Setup.TRACK)) && !isPickup) {
1703                return Setup.isPrintHeadersEnabled()
1704                        ? padAndTruncateIfNeeded(rs.getSplitDestinationTrackName(),
1705                                locationManager.getMaxTrackNameLength())
1706                        : padAndTruncateIfNeeded(
1707                                TrainManifestText.getStringTo() +
1708                                        SPACE +
1709                                        rs.getSplitDestinationTrackName(),
1710                                locationManager.getMaxTrackNameLength() +
1711                                        TrainManifestText.getStringTo().length() +
1712                                        1);
1713            } else if (attribute.equals(Setup.DEST_TRACK)) {
1714                // format destination name and destination track name
1715                String destAndTrackName =
1716                        rs.getSplitDestinationName() + "," + SPACE + rs.getSplitDestinationTrackName();
1717                return Setup.isPrintHeadersEnabled()
1718                        ? padAndTruncateIfNeeded(destAndTrackName,
1719                                locationManager.getMaxLocationAndTrackNameLength() + 2)
1720                        : padAndTruncateIfNeeded(TrainManifestText.getStringDest() + SPACE + destAndTrackName,
1721                                locationManager.getMaxLocationAndTrackNameLength() +
1722                                        TrainManifestText.getStringDest().length() +
1723                                        3);
1724            } else if (attribute.equals(Setup.OWNER)) {
1725                return padAndTruncateIfNeeded(rs.getOwnerName(),
1726                        InstanceManager.getDefault(CarOwners.class).getMaxNameLength());
1727            } else if (attribute.equals(Setup.LAST_TRAIN)) {
1728                String lastTrainName = padAndTruncateIfNeeded(rs.getLastTrainName(),
1729                        InstanceManager.getDefault(TrainManager.class).getMaxTrainNameLength());
1730                return Setup.isPrintHeadersEnabled() ? lastTrainName
1731                        : TrainManifestHeaderText.getStringHeader_Last_Train() + SPACE + lastTrainName;
1732            }
1733            // the three utility attributes that don't get printed but need to
1734              // be tabbed out
1735            else if (attribute.equals(Setup.NO_NUMBER)) {
1736                return padAndTruncateIfNeeded("",
1737                        Control.max_len_string_print_road_number - (UTILITY_CAR_COUNT_FIELD_SIZE + 1));
1738            } else if (attribute.equals(Setup.NO_ROAD)) {
1739                return padAndTruncateIfNeeded("", InstanceManager.getDefault(CarRoads.class).getMaxNameLength());
1740            } else if (attribute.equals(Setup.NO_COLOR)) {
1741                return padAndTruncateIfNeeded("", InstanceManager.getDefault(CarColors.class).getMaxNameLength());
1742            } // there are four truncated manifest attributes
1743            else if (attribute.equals(Setup.NO_DEST_TRACK)) {
1744                return Setup.isPrintHeadersEnabled()
1745                        ? padAndTruncateIfNeeded("", locationManager.getMaxLocationAndTrackNameLength() + 1)
1746                        : "";
1747            } else if ((attribute.equals(Setup.NO_LOCATION) && !isPickup) ||
1748                    (attribute.equals(Setup.NO_DESTINATION) && isPickup)) {
1749                return Setup.isPrintHeadersEnabled()
1750                        ? padAndTruncateIfNeeded("", locationManager.getMaxLocationNameLength())
1751                        : "";
1752            } else if (attribute.equals(Setup.NO_TRACK) ||
1753                    attribute.equals(Setup.NO_LOCATION) ||
1754                    attribute.equals(Setup.NO_DESTINATION)) {
1755                return Setup.isPrintHeadersEnabled()
1756                        ? padAndTruncateIfNeeded("", locationManager.getMaxTrackNameLength())
1757                        : "";
1758            } else if (attribute.equals(Setup.TAB)) {
1759                return createTabIfNeeded(Setup.getTab1Length() - 1);
1760            } else if (attribute.equals(Setup.TAB2)) {
1761                return createTabIfNeeded(Setup.getTab2Length() - 1);
1762            } else if (attribute.equals(Setup.TAB3)) {
1763                return createTabIfNeeded(Setup.getTab3Length() - 1);
1764            }
1765            // something isn't right!
1766            return Bundle.getMessage("ErrorPrintOptions", attribute);
1767
1768        } catch (ArrayIndexOutOfBoundsException e) {
1769            if (attribute.equals(Setup.ROAD)) {
1770                return padAndTruncateIfNeeded("", InstanceManager.getDefault(CarRoads.class).getMaxNameLength());
1771            } else if (attribute.equals(Setup.TYPE)) {
1772                return padAndTruncateIfNeeded("", InstanceManager.getDefault(CarTypes.class).getMaxNameLength());
1773            }
1774            // something isn't right!
1775            return Bundle.getMessage("ErrorPrintOptions", attribute);
1776        }
1777    }
1778
1779    /**
1780     * Two column header format. Left side pick ups, right side set outs
1781     *
1782     * @param file       Manifest or switch list File.
1783     * @param isManifest True if manifest, false if switch list.
1784     */
1785    public void printEngineHeader(PrintWriter file, boolean isManifest) {
1786        int lineLength = getLineLength(isManifest);
1787        printHorizontalLine(file, isManifest);
1788        if (Setup.isPrintHeadersEnabled()) {
1789            if (!Setup.getPickupEnginePrefix().isBlank() || !Setup.getDropEnginePrefix().isBlank()) {
1790                // center engine pick up and set out text
1791                String s = padAndTruncate(tabString(Setup.getPickupEnginePrefix().trim(),
1792                        lineLength / 4 - Setup.getPickupEnginePrefix().length() / 2), lineLength / 2) +
1793                        VERTICAL_LINE_CHAR +
1794                        tabString(Setup.getDropEnginePrefix(),
1795                                lineLength / 4 - Setup.getDropEnginePrefix().length() / 2);
1796                s = padAndTruncate(s, lineLength);
1797                addLine(file, s);
1798                printHorizontalLine1(file, isManifest);
1799            }
1800
1801            String s = padAndTruncate(getPickupEngineHeader(), lineLength / 2);
1802            s = padAndTruncate(s + VERTICAL_LINE_CHAR + getDropEngineHeader(), lineLength);
1803            addLine(file, s);
1804            printHorizontalLine2(file, isManifest);
1805        }
1806    }
1807
1808    public void printPickupEngineHeader(PrintWriter file, boolean isManifest) {
1809        int lineLength = getLineLength(isManifest);
1810        printHorizontalLine1(file, isManifest);
1811        String s = padAndTruncate(createTabIfNeeded(Setup.getManifestPrefixLength() + 1) + getPickupEngineHeader(),
1812                lineLength);
1813        addLine(file, s);
1814        printHorizontalLine2(file, isManifest);
1815    }
1816
1817    public void printDropEngineHeader(PrintWriter file, boolean isManifest) {
1818        int lineLength = getLineLength(isManifest);
1819        printHorizontalLine1(file, isManifest);
1820        String s = padAndTruncate(createTabIfNeeded(Setup.getManifestPrefixLength() + 1) + getDropEngineHeader(),
1821                lineLength);
1822        addLine(file, s);
1823        printHorizontalLine2(file, isManifest);
1824    }
1825
1826    /**
1827     * Prints the two column header for cars. Left side pick ups, right side set
1828     * outs.
1829     *
1830     * @param file             Manifest or Switch List File
1831     * @param isManifest       True if manifest, false if switch list.
1832     * @param isTwoColumnTrack True if two column format using track names.
1833     */
1834    public void printCarHeader(PrintWriter file, boolean isManifest, boolean isTwoColumnTrack) {
1835        int lineLength = getLineLength(isManifest);
1836        printHorizontalLine(file, isManifest);
1837        if (Setup.isPrintHeadersEnabled()) {
1838            // center pick up and set out text
1839            String s = padAndTruncate(
1840                    tabString(Setup.getPickupCarPrefix(), lineLength / 4 - Setup.getPickupCarPrefix().length() / 2),
1841                    lineLength / 2) +
1842                    VERTICAL_LINE_CHAR +
1843                    tabString(Setup.getDropCarPrefix(), lineLength / 4 - Setup.getDropCarPrefix().length() / 2);
1844            s = padAndTruncate(s, lineLength);
1845            addLine(file, s);
1846            printHorizontalLine1(file, isManifest);
1847
1848            s = padAndTruncate(getPickupCarHeader(isManifest, isTwoColumnTrack), lineLength / 2);
1849            s = padAndTruncate(s + VERTICAL_LINE_CHAR + getDropCarHeader(isManifest, isTwoColumnTrack), lineLength);
1850            addLine(file, s);
1851            printHorizontalLine2(file, isManifest);
1852        }
1853    }
1854
1855    public void printPickupCarHeader(PrintWriter file, boolean isManifest, boolean isTwoColumnTrack) {
1856        if (Setup.isPrintHeadersEnabled()) {
1857            printHorizontalLine1(file, isManifest);
1858            String s = padAndTruncate(createTabIfNeeded(Setup.getManifestPrefixLength() + 1) +
1859                    getPickupCarHeader(isManifest, isTwoColumnTrack), getLineLength(isManifest));
1860            addLine(file, s);
1861            printHorizontalLine2(file, isManifest);
1862        }
1863    }
1864
1865    public void printDropCarHeader(PrintWriter file, boolean isManifest, boolean isTwoColumnTrack) {
1866        if (!Setup.isPrintHeadersEnabled() || getDropCarHeader(isManifest, isTwoColumnTrack).isBlank()) {
1867            return;
1868        }
1869        printHorizontalLine1(file, isManifest);
1870        String s = padAndTruncate(
1871                createTabIfNeeded(Setup.getManifestPrefixLength() + 1) + getDropCarHeader(isManifest, isTwoColumnTrack),
1872                getLineLength(isManifest));
1873        addLine(file, s);
1874        printHorizontalLine2(file, isManifest);
1875    }
1876
1877    public void printLocalCarMoveHeader(PrintWriter file, boolean isManifest) {
1878        if (!Setup.isPrintHeadersEnabled()) {
1879            return;
1880        }
1881        printHorizontalLine1(file, isManifest);
1882        String s = padAndTruncate(
1883                createTabIfNeeded(Setup.getManifestPrefixLength() + 1) + getLocalMoveHeader(isManifest),
1884                getLineLength(isManifest));
1885        addLine(file, s);
1886        printHorizontalLine2(file, isManifest);
1887    }
1888
1889    public String getPickupEngineHeader() {
1890        return getHeader(Setup.getPickupEngineMessageFormat(), PICKUP, !LOCAL, ENGINE);
1891    }
1892
1893    public String getDropEngineHeader() {
1894        return getHeader(Setup.getDropEngineMessageFormat(), !PICKUP, !LOCAL, ENGINE);
1895    }
1896
1897    public String getPickupCarHeader(boolean isManifest, boolean isTwoColumnTrack) {
1898        if (isManifest && !isTwoColumnTrack) {
1899            return getHeader(Setup.getPickupManifestMessageFormat(), PICKUP, !LOCAL, !ENGINE);
1900        } else if (!isManifest && !isTwoColumnTrack) {
1901            return getHeader(Setup.getPickupSwitchListMessageFormat(), PICKUP, !LOCAL, !ENGINE);
1902        } else if (isManifest && isTwoColumnTrack) {
1903            return getHeader(Setup.getPickupTwoColumnByTrackManifestMessageFormat(), PICKUP, !LOCAL, !ENGINE);
1904        } else {
1905            return getHeader(Setup.getPickupTwoColumnByTrackSwitchListMessageFormat(), PICKUP, !LOCAL, !ENGINE);
1906        }
1907    }
1908
1909    public String getDropCarHeader(boolean isManifest, boolean isTwoColumnTrack) {
1910        if (isManifest && !isTwoColumnTrack) {
1911            return getHeader(Setup.getDropManifestMessageFormat(), !PICKUP, !LOCAL, !ENGINE);
1912        } else if (!isManifest && !isTwoColumnTrack) {
1913            return getHeader(Setup.getDropSwitchListMessageFormat(), !PICKUP, !LOCAL, !ENGINE);
1914        } else if (isManifest && isTwoColumnTrack) {
1915            return getHeader(Setup.getDropTwoColumnByTrackManifestMessageFormat(), !PICKUP, !LOCAL, !ENGINE);
1916        } else {
1917            return getHeader(Setup.getDropTwoColumnByTrackSwitchListMessageFormat(), !PICKUP, !LOCAL, !ENGINE);
1918        }
1919    }
1920
1921    public String getLocalMoveHeader(boolean isManifest) {
1922        if (isManifest) {
1923            return getHeader(Setup.getLocalManifestMessageFormat(), !PICKUP, LOCAL, !ENGINE);
1924        } else {
1925            return getHeader(Setup.getLocalSwitchListMessageFormat(), !PICKUP, LOCAL, !ENGINE);
1926        }
1927    }
1928
1929    private String getHeader(String[] format, boolean isPickup, boolean isLocal, boolean isEngine) {
1930        StringBuffer buf = new StringBuffer();
1931        for (String attribute : format) {
1932            if (attribute.equals(Setup.BLANK)) {
1933                continue;
1934            }
1935            if (attribute.equals(Setup.ROAD)) {
1936                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Road(),
1937                        InstanceManager.getDefault(CarRoads.class).getMaxNameLength()) + SPACE);
1938            } else if (attribute.equals(Setup.NUMBER) && !isEngine) {
1939                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Number(),
1940                        Control.max_len_string_print_road_number) + SPACE);
1941            } else if (attribute.equals(Setup.NUMBER) && isEngine) {
1942                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_EngineNumber(),
1943                        Control.max_len_string_print_road_number) + SPACE);
1944            } else if (attribute.equals(Setup.TYPE)) {
1945                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Type(),
1946                        InstanceManager.getDefault(CarTypes.class).getMaxNameLength()) + SPACE);
1947            } else if (attribute.equals(Setup.MODEL)) {
1948                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Model(),
1949                        InstanceManager.getDefault(EngineModels.class).getMaxNameLength()) + SPACE);
1950            } else if (attribute.equals(Setup.HP)) {
1951                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Hp(),
1952                        5) + SPACE);
1953            } else if (attribute.equals(Setup.CONSIST)) {
1954                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Consist(),
1955                        InstanceManager.getDefault(ConsistManager.class).getMaxNameLength()) + SPACE);
1956            } else if (attribute.equals(Setup.DCC_ADDRESS)) {
1957                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_DCC_Address(),
1958                        TrainManifestHeaderText.getStringHeader_DCC_Address().length()) + SPACE);
1959            } else if (attribute.equals(Setup.KERNEL)) {
1960                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Kernel(),
1961                        InstanceManager.getDefault(KernelManager.class).getMaxNameLength()) + SPACE);
1962            } else if (attribute.equals(Setup.KERNEL_SIZE)) {
1963                buf.append("   "); // assume kernel size is 99 or less
1964            } else if (attribute.equals(Setup.LOAD)) {
1965                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Load(),
1966                        InstanceManager.getDefault(CarLoads.class).getMaxNameLength()) + SPACE);
1967            } else if (attribute.equals(Setup.LOAD_TYPE)) {
1968                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Load_Type(),
1969                        TrainManifestHeaderText.getStringHeader_Load_Type().length()) + SPACE);
1970            } else if (attribute.equals(Setup.COLOR)) {
1971                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Color(),
1972                        InstanceManager.getDefault(CarColors.class).getMaxNameLength()) + SPACE);
1973            } else if (attribute.equals(Setup.OWNER)) {
1974                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Owner(),
1975                        InstanceManager.getDefault(CarOwners.class).getMaxNameLength()) + SPACE);
1976            } else if (attribute.equals(Setup.LENGTH)) {
1977                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Length(),
1978                        InstanceManager.getDefault(CarLengths.class).getMaxNameLength()) + SPACE);
1979            } else if (attribute.equals(Setup.WEIGHT)) {
1980                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Weight(),
1981                        Control.max_len_string_weight_name) + SPACE);
1982            } else if (attribute.equals(Setup.TRACK)) {
1983                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Track(),
1984                        locationManager.getMaxTrackNameLength()) + SPACE);
1985            } else if (attribute.equals(Setup.LOCATION) && (isPickup || isLocal)) {
1986                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Location(),
1987                        locationManager.getMaxTrackNameLength()) + SPACE);
1988            } else if (attribute.equals(Setup.LOCATION) && !isPickup) {
1989                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Location(),
1990                        locationManager.getMaxLocationNameLength()) + SPACE);
1991            } else if (attribute.equals(Setup.DESTINATION) && !isPickup) {
1992                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Destination(),
1993                        locationManager.getMaxTrackNameLength()) + SPACE);
1994            } else if (attribute.equals(Setup.DESTINATION) && isPickup) {
1995                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Destination(),
1996                        locationManager.getMaxLocationNameLength()) + SPACE);
1997            } else if (attribute.equals(Setup.DEST_TRACK)) {
1998                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Dest_Track(),
1999                        locationManager.getMaxLocationAndTrackNameLength() + 2) + SPACE);
2000            } else if (attribute.equals(Setup.FINAL_DEST)) {
2001                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Final_Dest(),
2002                        locationManager.getMaxLocationNameLength()) + SPACE);
2003            } else if (attribute.equals(Setup.FINAL_DEST_TRACK)) {
2004                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Final_Dest_Track(),
2005                        locationManager.getMaxLocationAndTrackNameLength() + 2) + SPACE);
2006            } else if (attribute.equals(Setup.HAZARDOUS)) {
2007                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Hazardous(),
2008                        Setup.getHazardousMsg().length()) + SPACE);
2009            } else if (attribute.equals(Setup.RWE)) {
2010                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_RWE(),
2011                        locationManager.getMaxLocationAndTrackNameLength()) + SPACE);
2012            } else if (attribute.equals(Setup.COMMENT)) {
2013                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Comment(),
2014                        isEngine ? engineManager.getMaxCommentLength() : carManager.getMaxCommentLength()) + SPACE);
2015            } else if (attribute.equals(Setup.DROP_COMMENT)) {
2016                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Drop_Comment(),
2017                        InstanceManager.getDefault(CarLoads.class).getMaxLoadCommentLength()) + SPACE);
2018            } else if (attribute.equals(Setup.PICKUP_COMMENT)) {
2019                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Pickup_Comment(),
2020                        InstanceManager.getDefault(CarLoads.class).getMaxLoadCommentLength()) + SPACE);
2021            } else if (attribute.equals(Setup.DIVISION)) {
2022                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Division(),
2023                        InstanceManager.getDefault(DivisionManager.class).getMaxDivisionNameLength()) + SPACE);
2024            } else if (attribute.equals(Setup.BLOCKING_ORDER)) {
2025                buf.append("    "); // assume blocking order +/- 99
2026            } else if (attribute.equals(Setup.LAST_TRAIN)) {
2027                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Last_Train(),
2028                        InstanceManager.getDefault(TrainManager.class).getMaxTrainNameLength()) + SPACE);
2029            } else if (attribute.equals(Setup.TAB)) {
2030                buf.append(createTabIfNeeded(Setup.getTab1Length()));
2031            } else if (attribute.equals(Setup.TAB2)) {
2032                buf.append(createTabIfNeeded(Setup.getTab2Length()));
2033            } else if (attribute.equals(Setup.TAB3)) {
2034                buf.append(createTabIfNeeded(Setup.getTab3Length()));
2035            } else {
2036                buf.append(attribute + SPACE);
2037            }
2038        }
2039        return buf.toString().stripTrailing();
2040    }
2041
2042    protected void printTrackNameHeader(PrintWriter file, String trackName, boolean isManifest) {
2043        printHorizontalLine(file, isManifest);
2044        int lineLength = getLineLength(isManifest);
2045        String s = padAndTruncate(tabString(trackName.trim(), lineLength / 4 - trackName.trim().length() / 2),
2046                lineLength / 2) +
2047                VERTICAL_LINE_CHAR +
2048                tabString(trackName.trim(), lineLength / 4 - trackName.trim().length() / 2);
2049        s = padAndTruncate(s, lineLength);
2050        addLine(file, s);
2051        if (Setup.isPrintHeaderLine3Enabled()) {
2052            printHorizontalLine(file, isManifest);
2053        }
2054    }
2055
2056    public void printHorizontalLine1(PrintWriter file, boolean isManifest) {
2057        if (Setup.isPrintHeaderLine1Enabled()) {
2058            printHorizontalLine(file, isManifest);
2059        }
2060    }
2061
2062    public void printHorizontalLine2(PrintWriter file, boolean isManifest) {
2063        if (Setup.isPrintHeaderLine2Enabled()) {
2064            printHorizontalLine(file, isManifest);
2065        }
2066    }
2067
2068    public void printHorizontalLine3(PrintWriter file, boolean isManifest) {
2069        if (Setup.isPrintHeadersEnabled() && Setup.isPrintHeaderLine3Enabled() ||
2070                !Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) {
2071            printHorizontalLine(file, isManifest);
2072        }
2073    }
2074
2075    /**
2076     * Prints a line across the entire page.
2077     *
2078     * @param file       The File to print to.
2079     * @param isManifest True if manifest, false if switch list.
2080     */
2081    public void printHorizontalLine(PrintWriter file, boolean isManifest) {
2082        printHorizontalLine(file, 0, getLineLength(isManifest));
2083    }
2084
2085    public void printHorizontalLine(PrintWriter file, int start, int end) {
2086        StringBuffer sb = new StringBuffer();
2087        while (start-- > 0) {
2088            sb.append(SPACE);
2089        }
2090        while (end-- > 0) {
2091            sb.append(HORIZONTAL_LINE_CHAR);
2092        }
2093        addLine(file, sb.toString());
2094    }
2095
2096    public static String getISO8601Date(boolean isModelYear) {
2097        Calendar calendar = Calendar.getInstance();
2098        // use the JMRI Timebase (which may be a fast clock).
2099        calendar.setTime(jmri.InstanceManager.getDefault(jmri.Timebase.class).getTime());
2100        if (isModelYear && !Setup.getYearModeled().isEmpty()) {
2101            try {
2102                calendar.set(Calendar.YEAR, Integer.parseInt(Setup.getYearModeled().trim()));
2103            } catch (NumberFormatException e) {
2104                return Setup.getYearModeled();
2105            }
2106        }
2107        return (new StdDateFormat()).format(calendar.getTime());
2108    }
2109
2110    public static String getDate(Date date) {
2111        SimpleDateFormat format = new SimpleDateFormat("M/dd/yyyy HH:mm"); // NOI18N
2112        if (Setup.is12hrFormatEnabled()) {
2113            format = new SimpleDateFormat("M/dd/yyyy hh:mm a"); // NOI18N
2114        }
2115        return format.format(date);
2116    }
2117
2118    public static String getDate(boolean isModelYear) {
2119        Calendar calendar = Calendar.getInstance();
2120        // use the JMRI Timebase (which may be a fast clock).
2121        calendar.setTime(jmri.InstanceManager.getDefault(jmri.Timebase.class).getTime());
2122        if (isModelYear && !Setup.getYearModeled().equals(Setup.NONE)) {
2123            try {
2124                calendar.set(Calendar.YEAR, Integer.parseInt(Setup.getYearModeled().trim()));
2125            } catch (NumberFormatException e) {
2126                return Setup.getYearModeled();
2127            }
2128        }
2129        return TrainCommon.getDate(calendar.getTime());
2130    }
2131
2132    public static Date convertStringToDate(String date) {
2133        if (!date.isBlank()) {
2134            // create a date object from the string.
2135            try {
2136                // try MM/dd/yyyy HH:mm:ss.
2137                SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss"); // NOI18N
2138                return formatter.parse(date);
2139            } catch (java.text.ParseException pe1) {
2140                // try the old 12 hour format (no seconds).
2141                try {
2142                    SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy hh:mmaa"); // NOI18N
2143                    return formatter.parse(date);
2144                } catch (java.text.ParseException pe2) {
2145                    try {
2146                        // try 24hour clock.
2147                        SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy HH:mm"); // NOI18N
2148                        return formatter.parse(date);
2149                    } catch (java.text.ParseException pe3) {
2150                        log.debug("Not able to parse date: {}", date);
2151                    }
2152                }
2153            }
2154        }
2155        return null; // there was no date specified.
2156    }
2157
2158    /*
2159     * Converts String time DAYS:HH:MM and DAYS:HH:MM AM/PM to minutes from
2160     * midnight. Note that the string time could be blank, and in that case
2161     * returns 0 minutes. 
2162     */
2163    public static int convertStringTime(String time) {
2164        int minutes = 0;
2165        boolean hrFormat = false;
2166        String[] splitTimePM = time.split(SPACE);
2167        if (splitTimePM.length > 1) {
2168            hrFormat = true;
2169            if (splitTimePM[1].equals(Bundle.getMessage("PM"))) {
2170                minutes = 12 * 60;
2171            }
2172        }
2173        String[] splitTime = splitTimePM[0].split(":");
2174
2175        if (splitTime.length > 2) {
2176            // days:hrs:minutes
2177            if (hrFormat && splitTime[1].equals("12")) {
2178                splitTime[1] = "00";
2179            }
2180            minutes += 24 * 60 * Integer.parseInt(splitTime[0]);
2181            minutes += 60 * Integer.parseInt(splitTime[1]);
2182            minutes += Integer.parseInt(splitTime[2]);
2183        } else if (splitTime.length  == 2){
2184            // hrs:minutes
2185            if (hrFormat && splitTime[0].equals("12")) {
2186                splitTime[0] = "00";
2187            }
2188            minutes += 60 * Integer.parseInt(splitTime[0]);
2189            minutes += Integer.parseInt(splitTime[1]);
2190        }
2191        log.debug("convert time {} to minutes {}", time, minutes);
2192        return minutes;
2193    }
2194    
2195    public String convertMinutesTime(int minutes) {
2196        int days = minutes / (24 * 60);
2197        int h = (minutes - (days * 24 * 60))/60;
2198        String sHours = String.format("%02d", h);
2199        int m = minutes - days * 24 * 60 - h * 60;
2200        String sMinutes = String.format("%02d", m);
2201        return Integer.toString(days) + ":" + sHours + ":" + sMinutes;
2202    }
2203        
2204
2205    /**
2206     * Pads out a string by adding spaces to the end of the string, and will
2207     * remove characters from the end of the string if the string exceeds the
2208     * field size.
2209     *
2210     * @param s         The string to pad.
2211     * @param fieldSize The maximum length of the string.
2212     * @return A String the specified length
2213     */
2214    public static String padAndTruncateIfNeeded(String s, int fieldSize) {
2215        if (Setup.isTabEnabled()) {
2216            return padAndTruncate(s, fieldSize);
2217        }
2218        return s;
2219    }
2220
2221    public static String padAndTruncate(String s, int fieldSize) {
2222        s = padString(s, fieldSize);
2223        if (s.length() > fieldSize) {
2224            s = s.substring(0, fieldSize);
2225        }
2226        return s;
2227    }
2228
2229    /**
2230     * Adjusts string to be a certain number of characters by adding spaces to
2231     * the end of the string.
2232     *
2233     * @param s         The string to pad
2234     * @param fieldSize The fixed length of the string.
2235     * @return A String the specified length
2236     */
2237    public static String padString(String s, int fieldSize) {
2238        StringBuffer buf = new StringBuffer(s);
2239        while (buf.length() < fieldSize) {
2240            buf.append(SPACE);
2241        }
2242        return buf.toString();
2243    }
2244
2245    /**
2246     * Creates a String of spaces to create a tab for text. Tabs must be
2247     * enabled. Setup.isTabEnabled()
2248     * 
2249     * @param tabSize the length of tab
2250     * @return tab
2251     */
2252    public static String createTabIfNeeded(int tabSize) {
2253        if (Setup.isTabEnabled()) {
2254            return tabString("", tabSize);
2255        }
2256        return "";
2257    }
2258
2259    protected static String tabString(String s, int tabSize) {
2260        StringBuffer buf = new StringBuffer();
2261        // TODO this doesn't consider the length of s string.
2262        while (buf.length() < tabSize) {
2263            buf.append(SPACE);
2264        }
2265        buf.append(s);
2266        return buf.toString();
2267    }
2268
2269    /**
2270     * Returns the line length for manifest or switch list printout. Always an
2271     * even number.
2272     * 
2273     * @param isManifest True if manifest.
2274     * @return line length for manifest or switch list.
2275     */
2276    public static int getLineLength(boolean isManifest) {
2277        return getLineLength(isManifest ? Setup.getManifestOrientation() : Setup.getSwitchListOrientation(),
2278                Setup.getFontName(), Font.PLAIN, Setup.getManifestFontSize());
2279    }
2280
2281    public static int getManifestHeaderLineLength() {
2282        return getLineLength(Setup.getManifestOrientation(), "SansSerif", Font.ITALIC, Setup.getManifestFontSize());
2283    }
2284
2285    private static int getLineLength(String orientation, String fontName, int fontStyle, int fontSize) {
2286        Integer charsPerLine = InstanceManager.getDefault(TrainManager.class).getHardcopyWriterLineLength(fontName,
2287                fontStyle, fontSize, getPageSize(orientation), orientation.equals(Setup.LANDSCAPE));
2288        if (charsPerLine == null) {
2289            // first try using hardcopywriter to get number of characters per line
2290            charsPerLine = getCharsPerLineHardcopyWriter(orientation, fontName, fontStyle, fontSize);
2291            if (charsPerLine == null) {
2292                charsPerLine = getCharsPerLine(orientation, fontName, fontStyle, fontSize);
2293            }
2294            log.debug("Number of characters per line {}, fontName: {}, fontStyle {}, fontSize {}", charsPerLine, fontName,
2295                    fontStyle, fontSize);
2296            InstanceManager.getDefault(TrainManager.class).setHardcopyWriterLineLength(fontName,
2297                    fontStyle, fontSize, getPageSize(orientation), orientation.equals(Setup.LANDSCAPE), charsPerLine);
2298        }
2299        return charsPerLine;
2300    }
2301    
2302    private static Integer getCharsPerLineHardcopyWriter(String orientation, String fontName, int fontStyle, int fontSize) {
2303        // obtain a HardcopyWriter to do this
2304        Dimension pageSize = null;
2305        if (orientation.equals(Setup.HANDHELD) || orientation.equals(Setup.HALFPAGE)) {
2306            // add margins to page size
2307            pageSize = new Dimension(getPageSize(orientation).width + PAPER_MARGINS.width,
2308                    getPageSize(orientation).height + PAPER_MARGINS.height);
2309        }
2310        Integer charsPerLine = null;
2311        try (HardcopyWriter writer = new HardcopyWriter(fontName, fontStyle, fontSize, .5 * 72, .5 * 72, .5 * 72, .5 * 72, orientation.equals(Setup.LANDSCAPE),
2312                pageSize)) {
2313
2314            charsPerLine = writer.getCharactersPerLine();
2315
2316        } catch (HardcopyWriter.PrintCanceledException ex) {
2317            log.debug("Print canceled");
2318        }
2319        log.debug("orientation: {}, fontName: {}, fontStyle: {}, fontSize {}, chars/line: {}", orientation, fontName,
2320                fontStyle, fontSize, charsPerLine);
2321        return charsPerLine;
2322    }
2323
2324    // backup method for determining characters per line
2325    private static int getCharsPerLine(String orientation, String fontName, int fontStyle, int fontSize) {
2326        Font font = new Font(fontName, fontStyle, fontSize); // NOI18N
2327        JLabel label = new JLabel();
2328        FontMetrics metrics = label.getFontMetrics(font);
2329        int charwidth = metrics.charWidth('m');
2330        if (charwidth == 0) {
2331            log.error("Line length charater width equal to zero. font size: {}, fontName: {}", fontSize, fontName);
2332            charwidth = fontSize / 2; // create a reasonable character width
2333        }
2334        // compute lines and columns within margins
2335        int charsPerLine = getPageSize(orientation).width / charwidth;
2336        return charsPerLine;
2337    }
2338
2339    private boolean checkStringLength(String string, boolean isManifest) {
2340        return checkStringLength(string, isManifest ? Setup.getManifestOrientation() : Setup.getSwitchListOrientation(),
2341                Setup.getFontName(), Setup.getManifestFontSize());
2342    }
2343
2344    /**
2345     * Checks to see if the string fits on the page.
2346     *
2347     * @return false if string length is longer than page width.
2348     */
2349    private boolean checkStringLength(String string, String orientation, String fontName, int fontSize) {
2350        // ignore text color controls when determining line length
2351        if (string.startsWith(TEXT_COLOR_START) && string.contains(TEXT_COLOR_DONE)) {
2352            string = string.substring(string.indexOf(TEXT_COLOR_DONE) + 2);
2353        }
2354        if (string.contains(TEXT_COLOR_END)) {
2355            string = string.substring(0, string.indexOf(TEXT_COLOR_END));
2356        }
2357        Font font = new Font(fontName, Font.PLAIN, fontSize); // NOI18N
2358        JLabel label = new JLabel();
2359        FontMetrics metrics = label.getFontMetrics(font);
2360        int stringWidth = metrics.stringWidth(string);
2361        return stringWidth <= getPageSize(orientation).width;
2362    }
2363
2364    protected static final Dimension PAPER_MARGINS = new Dimension(84, 72);
2365
2366    public static Dimension getPageSize(String orientation) {
2367        // page size has been adjusted to account for margins of .5
2368        // Dimension(84, 72)
2369        Dimension pagesize = new Dimension(523, 720); // Portrait 8.5 x 11
2370        // landscape has .65 margins
2371        if (orientation.equals(Setup.LANDSCAPE)) {
2372            pagesize = new Dimension(702, 523); // 11 x 8.5
2373        }
2374        if (orientation.equals(Setup.HALFPAGE)) {
2375            pagesize = new Dimension(261, 720); // 4.25 x 11
2376        }
2377        if (orientation.equals(Setup.HANDHELD)) {
2378            pagesize = new Dimension(206, 720); // 3.25 x 11
2379        }
2380        return pagesize;
2381    }
2382
2383    /**
2384     * Produces a string using commas and spaces between the strings provided in
2385     * the array. Does not check for embedded commas in the string array.
2386     *
2387     * @param array The string array to be formated.
2388     * @return formated string using commas and spaces
2389     */
2390    public static String formatStringToCommaSeparated(String[] array) {
2391        StringBuffer sbuf = new StringBuffer("");
2392        for (String s : array) {
2393            if (s != null) {
2394                sbuf = sbuf.append(s + "," + SPACE);
2395            }
2396        }
2397        if (sbuf.length() > 2) {
2398            sbuf.setLength(sbuf.length() - 2); // remove trailing separators
2399        }
2400        return sbuf.toString();
2401    }
2402
2403    private void addLine(PrintWriter file, StringBuffer buf, Color color) {
2404        String s = buf.toString();
2405        if (!s.isBlank()) {
2406            addLine(file, formatColorString(s, color));
2407        }
2408    }
2409
2410    /**
2411     * Adds HTML like color text control characters around a string. Note that
2412     * black is the standard text color, and if black is requested no control
2413     * characters are added.
2414     * 
2415     * @param text  the text to be modified
2416     * @param color the color the text is to be printed
2417     * @return formated text with color modifiers
2418     */
2419    public static String formatColorString(String text, Color color) {
2420        String s = text;
2421        if (!color.equals(Color.black)) {
2422            s = TEXT_COLOR_START + ColorUtil.colorToColorName(color) + TEXT_COLOR_DONE + text + TEXT_COLOR_END;
2423        }
2424        return s;
2425    }
2426
2427    /**
2428     * Removes the color text control characters around the desired string
2429     * 
2430     * @param string the string with control characters
2431     * @return pure text
2432     */
2433    public static String getTextColorString(String string) {
2434        String text = string;
2435        if (string.contains(TEXT_COLOR_START)) {
2436            text = string.substring(0, string.indexOf(TEXT_COLOR_START)) +
2437                    string.substring(string.indexOf(TEXT_COLOR_DONE) + 2);
2438        }
2439        if (text.contains(TEXT_COLOR_END)) {
2440            text = text.substring(0, text.indexOf(TEXT_COLOR_END)) +
2441                    string.substring(string.indexOf(TEXT_COLOR_END) + TEXT_COLOR_END.length());
2442        }
2443        return text;
2444    }
2445
2446    public static Color getTextColor(String string) {
2447        Color color = Color.black;
2448        if (string.contains(TEXT_COLOR_START)) {
2449            String c = string.substring(string.indexOf(TEXT_COLOR_START) + TEXT_COLOR_START.length());
2450            c = c.substring(0, c.indexOf("\""));
2451            try {
2452                color = ColorUtil.stringToColor(c);
2453            } catch (IllegalArgumentException e) {
2454                log.error("Exception when getting text color: {}", string, e);
2455            }
2456        }
2457        return color;
2458    }
2459
2460    public static String getTextColorName(String string) {
2461        return ColorUtil.colorToColorName(getTextColor(string));
2462    }
2463
2464    private static final Logger log = LoggerFactory.getLogger(TrainCommon.class);
2465}