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