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