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 String HORIZONTAL_LINE_CHAR = "-";
041    protected static final String BUILD_REPORT_CHAR = "-";
042    public static final String HYPHEN = "-";
043    protected static final String 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.getPickupColor());
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.getDropColor());
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.getPickupColor());
127                buf = new StringBuffer(TAB); // new line
128            }
129            buf.append(s);
130        }
131        addLine(file, buf, Setup.getPickupColor());
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.getDropColor());
164                buf = new StringBuffer(TAB); // new line
165            }
166            buf.append(s);
167        }
168        addLine(file, buf, Setup.getDropColor());
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    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "SLF4J_FORMAT_SHOULD_BE_CONST",
615            justification = "Only when exception")
616    public static String getTrainMessage(Train train, RouteLocation rl) {
617        String expectedArrivalTime = train.getExpectedArrivalTime(rl);
618        String routeLocationName = rl.getSplitName();
619        String msg = "";
620        String messageFormatText = ""; // the text being formated in case there's an exception
621        try {
622            // Scheduled work at {0}
623            msg = MessageFormat.format(messageFormatText = TrainManifestText
624                    .getStringScheduledWork(),
625                    new Object[]{routeLocationName, train.getName(),
626                            train.getDescription(), rl.getLocation().getDivisionName()});
627            if (train.isShowArrivalAndDepartureTimesEnabled()) {
628                if (rl == train.getTrainDepartsRouteLocation()) {
629                    // Scheduled work at {0}, departure time {1}
630                    msg = MessageFormat.format(messageFormatText = TrainManifestText
631                            .getStringWorkDepartureTime(),
632                            new Object[]{routeLocationName,
633                                    train.getFormatedDepartureTime(), train.getName(),
634                                    train.getDescription(), rl.getLocation().getDivisionName()});
635                } else if (!rl.getDepartureTime().equals(RouteLocation.NONE)) {
636                    // Scheduled work at {0}, departure time {1}
637                    msg = MessageFormat.format(messageFormatText = TrainManifestText
638                            .getStringWorkDepartureTime(),
639                            new Object[]{routeLocationName,
640                                    rl.getFormatedDepartureTime(), train.getName(), train.getDescription(),
641                                    rl.getLocation().getDivisionName()});
642                } else if (Setup.isUseDepartureTimeEnabled() &&
643                        rl != train.getTrainTerminatesRouteLocation() &&
644                        !train.getExpectedDepartureTime(rl).equals(Train.ALREADY_SERVICED)) {
645                    // Scheduled work at {0}, departure time {1}
646                    msg = MessageFormat.format(messageFormatText = TrainManifestText
647                            .getStringWorkDepartureTime(),
648                            new Object[]{routeLocationName,
649                                    train.getExpectedDepartureTime(rl), train.getName(),
650                                    train.getDescription(), rl.getLocation().getDivisionName()});
651                } else if (!expectedArrivalTime.equals(Train.ALREADY_SERVICED)) {
652                    // Scheduled work at {0}, arrival time {1}
653                    msg = MessageFormat.format(messageFormatText = TrainManifestText
654                            .getStringWorkArrivalTime(),
655                            new Object[]{routeLocationName, expectedArrivalTime,
656                                    train.getName(), train.getDescription(),
657                                    rl.getLocation().getDivisionName()});
658                }
659            }
660            return msg;
661        } catch (IllegalArgumentException e) {
662            msg = Bundle.getMessage("ErrorIllegalArgument",
663                    Bundle.getMessage("TitleSwitchListText"), e.getLocalizedMessage()) + NEW_LINE + messageFormatText;
664            log.error(msg);
665            log.error("Illegal argument", e);
666            return msg;
667        }
668    }
669
670    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "SLF4J_FORMAT_SHOULD_BE_CONST",
671            justification = "Only when exception")
672    public static String getSwitchListTrainStatus(Train train, RouteLocation rl) {
673        String expectedArrivalTime = train.getExpectedArrivalTime(rl);
674        String msg = "";
675        String messageFormatText = ""; // the text being formated in case there's an exception
676        try {
677            if (train.isLocalSwitcher()) {
678                // Use Manifest text for local departure
679                // Scheduled work at {0}, departure time {1}
680                msg = MessageFormat.format(messageFormatText = TrainManifestText.getStringWorkDepartureTime(),
681                        new Object[]{splitString(train.getTrainDepartsName()), train.getFormatedDepartureTime(),
682                                train.getName(), train.getDescription(),
683                                rl.getLocation().getDivisionName()});
684            } else if (rl == train.getTrainDepartsRouteLocation()) {
685                // Departs {0} {1}bound at {2}
686                msg = MessageFormat.format(messageFormatText = TrainSwitchListText.getStringDepartsAt(),
687                        new Object[]{splitString(train.getTrainDepartsName()), rl.getTrainDirectionString(),
688                                train.getFormatedDepartureTime()});
689            } else if (Setup.isUseSwitchListDepartureTimeEnabled() &&
690                    rl != train.getTrainTerminatesRouteLocation() &&
691                    !expectedArrivalTime.equals(Train.ALREADY_SERVICED)) {
692                // Departs {0} at {1} expected arrival {2}, arrives {3}bound
693                msg = MessageFormat.format(
694                        messageFormatText = TrainSwitchListText.getStringDepartsAtExpectedArrival(),
695                        new Object[]{splitString(rl.getName()),
696                                train.getExpectedDepartureTime(rl), expectedArrivalTime,
697                                rl.getTrainDirectionString()});
698            } else if (train.isTrainEnRoute()) {
699                if (!expectedArrivalTime.equals(Train.ALREADY_SERVICED)) {
700                    // Departed {0}, expect to arrive in {1}, arrives {2}bound
701                    msg = MessageFormat.format(messageFormatText = TrainSwitchListText.getStringDepartedExpected(),
702                            new Object[]{splitString(train.getTrainDepartsName()), expectedArrivalTime,
703                                    rl.getTrainDirectionString(), train.getCurrentLocationName()});
704                }
705            } else {
706                // Departs {0} at {1} expected arrival {2}, arrives {3}bound
707                msg = MessageFormat.format(
708                        messageFormatText = TrainSwitchListText.getStringDepartsAtExpectedArrival(),
709                        new Object[]{splitString(train.getTrainDepartsName()),
710                                train.getFormatedDepartureTime(), expectedArrivalTime,
711                                rl.getTrainDirectionString()});
712            }
713            return msg;
714        } catch (IllegalArgumentException e) {
715            msg = Bundle.getMessage("ErrorIllegalArgument",
716                    Bundle.getMessage("TitleSwitchListText"), e.getLocalizedMessage()) + NEW_LINE + messageFormatText;
717            log.error(msg);
718            log.error("Illegal argument", e);
719            return msg;
720        }
721    }
722
723    int index = 0;
724
725    /*
726     * Used by two column format. Local moves (pulls and spots) are lined up
727     * when using this format,
728     */
729    private String appendSetoutString(String s, List<Car> carList, RouteLocation rl, boolean local, boolean isManifest,
730            boolean isTwoColumnTrack) {
731        while (index < carList.size()) {
732            Car car = carList.get(index++);
733            if (local && car.isLocalMove()) {
734                continue; // skip local moves
735            }
736            // car list is already sorted by destination track
737            if (car.getRouteDestination() == rl) {
738                String so = appendSetoutString(s, carList, rl, car, isManifest, isTwoColumnTrack);
739                // check for utility car
740                if (!so.equals(s)) {
741                    return so;
742                }
743            }
744        }
745        // no set out for this line
746        return s + VERTICAL_LINE_CHAR + padAndTruncate("", getLineLength(isManifest) / 2 - 1);
747    }
748
749    /*
750     * Used by two column, track names shown in the columns.
751     */
752    private String appendSetoutString(String s, String trackName, List<Car> carList, RouteLocation rl,
753            boolean isManifest, boolean isTwoColumnTrack) {
754        for (Car car : carList) {
755            if (!doneCars.contains(car) &&
756                    car.getRouteDestination() == rl &&
757                    trackName.equals(car.getSplitDestinationTrackName())) {
758                doneCars.add(car);
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 track
767        return s + VERTICAL_LINE_CHAR + padAndTruncate("", getLineLength(isManifest) / 2 - 1);
768    }
769
770    /*
771     * Appends to string the vertical line character, and the car set out
772     * string. Used in two column format.
773     */
774    private String appendSetoutString(String s, List<Car> carList, RouteLocation rl, Car car, boolean isManifest,
775            boolean isTwoColumnTrack) {
776        _dropCars = true;
777        String dropText;
778
779        if (car.isUtility()) {
780            dropText = setoutUtilityCars(carList, car, !LOCAL, isManifest, isTwoColumnTrack);
781            if (dropText == null) {
782                return s; // no changes to the input string
783            }
784        } else {
785            dropText = dropCar(car, isManifest, isTwoColumnTrack).trim();
786        }
787
788        dropText = padAndTruncate(dropText.trim(), getLineLength(isManifest) / 2 - 1);
789        dropText = formatColorString(dropText, car.isLocalMove() ? Setup.getLocalColor() : Setup.getDropColor());
790        return s + VERTICAL_LINE_CHAR + dropText;
791    }
792
793    /**
794     * Adds the car's pick up string to the output file using the truncated
795     * manifest format
796     *
797     * @param file       Manifest or switch list File
798     * @param car        The car being printed.
799     * @param isManifest True if manifest, false if switch list.
800     */
801    protected void pickUpCarTruncated(PrintWriter file, Car car, boolean isManifest) {
802        pickUpCar(file, car,
803                new StringBuffer(padAndTruncateIfNeeded(Setup.getPickupCarPrefix(), Setup.getManifestPrefixLength())),
804                Setup.getPickupTruncatedManifestMessageFormat(), isManifest);
805    }
806
807    /**
808     * Adds the car's pick up string to the output file using the manifest or
809     * switch list format
810     *
811     * @param file       Manifest or switch list File
812     * @param car        The car being printed.
813     * @param isManifest True if manifest, false if switch list.
814     */
815    protected void pickUpCar(PrintWriter file, Car car, boolean isManifest) {
816        if (isManifest) {
817            pickUpCar(file, car,
818                    new StringBuffer(
819                            padAndTruncateIfNeeded(Setup.getPickupCarPrefix(), Setup.getManifestPrefixLength())),
820                    Setup.getPickupManifestMessageFormat(), isManifest);
821        } else {
822            pickUpCar(file, car, new StringBuffer(
823                    padAndTruncateIfNeeded(Setup.getSwitchListPickupCarPrefix(), Setup.getSwitchListPrefixLength())),
824                    Setup.getPickupSwitchListMessageFormat(), isManifest);
825        }
826    }
827
828    private void pickUpCar(PrintWriter file, Car car, StringBuffer buf, String[] format, boolean isManifest) {
829        if (car.isLocalMove()) {
830            return; // print nothing local move, see dropCar
831        }
832        for (String attribute : format) {
833            String s = getCarAttribute(car, attribute, PICKUP, !LOCAL);
834            if (!checkStringLength(buf.toString() + s, isManifest)) {
835                addLine(file, buf, Setup.getPickupColor());
836                buf = new StringBuffer(TAB); // new line
837            }
838            buf.append(s);
839        }
840        addLine(file, buf, Setup.getPickupColor());
841    }
842
843    /**
844     * Returns the pick up car string. Useful for frames like train conductor
845     * and yardmaster.
846     *
847     * @param car              The car being printed.
848     * @param isManifest       when true use manifest format, when false use
849     *                         switch list format
850     * @param isTwoColumnTrack True if printing using two column format sorted
851     *                         by track name.
852     * @return pick up car string
853     */
854    public String pickupCar(Car car, boolean isManifest, boolean isTwoColumnTrack) {
855        StringBuffer buf = new StringBuffer();
856        String[] format;
857        if (isManifest && !isTwoColumnTrack) {
858            format = Setup.getPickupManifestMessageFormat();
859        } else if (!isManifest && !isTwoColumnTrack) {
860            format = Setup.getPickupSwitchListMessageFormat();
861        } else if (isManifest && isTwoColumnTrack) {
862            format = Setup.getPickupTwoColumnByTrackManifestMessageFormat();
863        } else {
864            format = Setup.getPickupTwoColumnByTrackSwitchListMessageFormat();
865        }
866        for (String attribute : format) {
867            buf.append(getCarAttribute(car, attribute, PICKUP, !LOCAL));
868        }
869        return buf.toString();
870    }
871
872    /**
873     * Adds the car's set out string to the output file using the truncated
874     * manifest format. Does not print out local moves. Local moves are only
875     * shown on the switch list for that location.
876     *
877     * @param file       Manifest or switch list File
878     * @param car        The car being printed.
879     * @param isManifest True if manifest, false if switch list.
880     */
881    protected void truncatedDropCar(PrintWriter file, Car car, boolean isManifest) {
882        // local move?
883        if (car.isLocalMove()) {
884            return; // yes, don't print local moves on train manifest
885        }
886        dropCar(file, car, new StringBuffer(Setup.getDropCarPrefix()), Setup.getDropTruncatedManifestMessageFormat(),
887                false, isManifest);
888    }
889
890    /**
891     * Adds the car's set out string to the output file using the manifest or
892     * switch list format
893     *
894     * @param file       Manifest or switch list File
895     * @param car        The car being printed.
896     * @param isManifest True if manifest, false if switch list.
897     */
898    protected void dropCar(PrintWriter file, Car car, boolean isManifest) {
899        boolean isLocal = car.isLocalMove();
900        if (isManifest) {
901            StringBuffer buf = new StringBuffer(
902                    padAndTruncateIfNeeded(Setup.getDropCarPrefix(), Setup.getManifestPrefixLength()));
903            String[] format = Setup.getDropManifestMessageFormat();
904            if (isLocal) {
905                buf = new StringBuffer(padAndTruncateIfNeeded(Setup.getLocalPrefix(), Setup.getManifestPrefixLength()));
906                format = Setup.getLocalManifestMessageFormat();
907            }
908            dropCar(file, car, buf, format, isLocal, isManifest);
909        } else {
910            StringBuffer buf = new StringBuffer(
911                    padAndTruncateIfNeeded(Setup.getSwitchListDropCarPrefix(), Setup.getSwitchListPrefixLength()));
912            String[] format = Setup.getDropSwitchListMessageFormat();
913            if (isLocal) {
914                buf = new StringBuffer(
915                        padAndTruncateIfNeeded(Setup.getSwitchListLocalPrefix(), Setup.getSwitchListPrefixLength()));
916                format = Setup.getLocalSwitchListMessageFormat();
917            }
918            dropCar(file, car, buf, format, isLocal, isManifest);
919        }
920    }
921
922    private void dropCar(PrintWriter file, Car car, StringBuffer buf, String[] format, boolean isLocal,
923            boolean isManifest) {
924        for (String attribute : format) {
925            String s = getCarAttribute(car, attribute, !PICKUP, isLocal);
926            if (!checkStringLength(buf.toString() + s, isManifest)) {
927                addLine(file, buf, isLocal ? Setup.getLocalColor() : Setup.getDropColor());
928                buf = new StringBuffer(TAB); // new line
929            }
930            buf.append(s);
931        }
932        addLine(file, buf, isLocal ? Setup.getLocalColor() : Setup.getDropColor());
933    }
934
935    /**
936     * Returns the drop car string. Useful for frames like train conductor and
937     * yardmaster.
938     *
939     * @param car              The car being printed.
940     * @param isManifest       when true use manifest format, when false use
941     *                         switch list format
942     * @param isTwoColumnTrack True if printing using two column format.
943     * @return drop car string
944     */
945    public String dropCar(Car car, boolean isManifest, boolean isTwoColumnTrack) {
946        StringBuffer buf = new StringBuffer();
947        String[] format;
948        if (isManifest && !isTwoColumnTrack) {
949            format = Setup.getDropManifestMessageFormat();
950        } else if (!isManifest && !isTwoColumnTrack) {
951            format = Setup.getDropSwitchListMessageFormat();
952        } else if (isManifest && isTwoColumnTrack) {
953            format = Setup.getDropTwoColumnByTrackManifestMessageFormat();
954        } else {
955            format = Setup.getDropTwoColumnByTrackSwitchListMessageFormat();
956        }
957        // TODO the Setup.Location doesn't work correctly for the conductor
958        // window due to the fact that the car can be in the train and not
959        // at its starting location.
960        // Therefore we use the local true to disable it.
961        boolean local = false;
962        if (car.getTrack() == null) {
963            local = true;
964        }
965        for (String attribute : format) {
966            buf.append(getCarAttribute(car, attribute, !PICKUP, local));
967        }
968        return buf.toString();
969    }
970
971    /**
972     * Returns the move car string. Useful for frames like train conductor and
973     * yardmaster.
974     *
975     * @param car        The car being printed.
976     * @param isManifest when true use manifest format, when false use switch
977     *                   list format
978     * @return move car string
979     */
980    public String localMoveCar(Car car, boolean isManifest) {
981        StringBuffer buf = new StringBuffer();
982        String[] format;
983        if (isManifest) {
984            format = Setup.getLocalManifestMessageFormat();
985        } else {
986            format = Setup.getLocalSwitchListMessageFormat();
987        }
988        for (String attribute : format) {
989            buf.append(getCarAttribute(car, attribute, !PICKUP, LOCAL));
990        }
991        return buf.toString();
992    }
993
994    List<String> utilityCarTypes = new ArrayList<>();
995    private static final int UTILITY_CAR_COUNT_FIELD_SIZE = 3;
996
997    /**
998     * Add a list of utility cars scheduled for pick up from the route location
999     * to the output file. The cars are blocked by destination.
1000     *
1001     * @param file       Manifest or Switch List File.
1002     * @param carList    List of cars for this train.
1003     * @param car        The utility car.
1004     * @param isTruncate True if manifest is to be truncated
1005     * @param isManifest True if manifest, false if switch list.
1006     */
1007    protected void pickupUtilityCars(PrintWriter file, List<Car> carList, Car car, boolean isTruncate,
1008            boolean isManifest) {
1009        // list utility cars by type, track, length, and load
1010        String[] format;
1011        if (isManifest) {
1012            format = Setup.getPickupUtilityManifestMessageFormat();
1013        } else {
1014            format = Setup.getPickupUtilitySwitchListMessageFormat();
1015        }
1016        if (isTruncate && isManifest) {
1017            format = Setup.createTruncatedManifestMessageFormat(format);
1018        }
1019        int count = countUtilityCars(format, carList, car, PICKUP);
1020        if (count == 0) {
1021            return; // already printed out this car type
1022        }
1023        pickUpCar(file, car,
1024                new StringBuffer(padAndTruncateIfNeeded(Setup.getPickupCarPrefix(),
1025                        isManifest ? Setup.getManifestPrefixLength() : Setup.getSwitchListPrefixLength()) +
1026                        SPACE +
1027                        padString(Integer.toString(count), UTILITY_CAR_COUNT_FIELD_SIZE)),
1028                format, isManifest);
1029    }
1030
1031    /**
1032     * Add a list of utility cars scheduled for drop at the route location to
1033     * the output file.
1034     *
1035     * @param file       Manifest or Switch List File.
1036     * @param carList    List of cars for this train.
1037     * @param car        The utility car.
1038     * @param isTruncate True if manifest is to be truncated
1039     * @param isManifest True if manifest, false if switch list.
1040     */
1041    protected void setoutUtilityCars(PrintWriter file, List<Car> carList, Car car, boolean isTruncate,
1042            boolean isManifest) {
1043        boolean isLocal = car.isLocalMove();
1044        StringBuffer buf;
1045        String[] format;
1046        if (isLocal && isManifest) {
1047            buf = new StringBuffer(padAndTruncateIfNeeded(Setup.getLocalPrefix(), Setup.getManifestPrefixLength()));
1048            format = Setup.getLocalUtilityManifestMessageFormat();
1049        } else if (!isLocal && isManifest) {
1050            buf = new StringBuffer(padAndTruncateIfNeeded(Setup.getDropCarPrefix(), Setup.getManifestPrefixLength()));
1051            format = Setup.getDropUtilityManifestMessageFormat();
1052        } else if (isLocal && !isManifest) {
1053            buf = new StringBuffer(
1054                    padAndTruncateIfNeeded(Setup.getSwitchListLocalPrefix(), Setup.getSwitchListPrefixLength()));
1055            format = Setup.getLocalUtilitySwitchListMessageFormat();
1056        } else {
1057            buf = new StringBuffer(
1058                    padAndTruncateIfNeeded(Setup.getSwitchListDropCarPrefix(), Setup.getSwitchListPrefixLength()));
1059            format = Setup.getDropUtilitySwitchListMessageFormat();
1060        }
1061        if (isTruncate && isManifest) {
1062            format = Setup.createTruncatedManifestMessageFormat(format);
1063        }
1064
1065        int count = countUtilityCars(format, carList, car, !PICKUP);
1066        if (count == 0) {
1067            return; // already printed out this car type
1068        }
1069        buf.append(SPACE + padString(Integer.toString(count), UTILITY_CAR_COUNT_FIELD_SIZE));
1070        dropCar(file, car, buf, format, isLocal, isManifest);
1071    }
1072
1073    public String pickupUtilityCars(List<Car> carList, Car car, boolean isManifest, boolean isTwoColumnTrack) {
1074        int count = countPickupUtilityCars(carList, car, isManifest);
1075        if (count == 0) {
1076            return null;
1077        }
1078        String[] format;
1079        if (isManifest && !isTwoColumnTrack) {
1080            format = Setup.getPickupUtilityManifestMessageFormat();
1081        } else if (!isManifest && !isTwoColumnTrack) {
1082            format = Setup.getPickupUtilitySwitchListMessageFormat();
1083        } else if (isManifest && isTwoColumnTrack) {
1084            format = Setup.getPickupTwoColumnByTrackUtilityManifestMessageFormat();
1085        } else {
1086            format = Setup.getPickupTwoColumnByTrackUtilitySwitchListMessageFormat();
1087        }
1088        StringBuffer buf = new StringBuffer(SPACE + padString(Integer.toString(count), UTILITY_CAR_COUNT_FIELD_SIZE));
1089        for (String attribute : format) {
1090            buf.append(getCarAttribute(car, attribute, PICKUP, !LOCAL));
1091        }
1092        return buf.toString();
1093    }
1094
1095    public int countPickupUtilityCars(List<Car> carList, Car car, boolean isManifest) {
1096        // list utility cars by type, track, length, and load
1097        String[] format;
1098        if (isManifest) {
1099            format = Setup.getPickupUtilityManifestMessageFormat();
1100        } else {
1101            format = Setup.getPickupUtilitySwitchListMessageFormat();
1102        }
1103        return countUtilityCars(format, carList, car, PICKUP);
1104    }
1105
1106    /**
1107     * For the Conductor and Yardmaster windows.
1108     *
1109     * @param carList    List of cars for this train.
1110     * @param car        The utility car.
1111     * @param isLocal    True if local move.
1112     * @param isManifest True if manifest, false if switch list.
1113     * @return A string representing the work of identical utility cars.
1114     */
1115    public String setoutUtilityCars(List<Car> carList, Car car, boolean isLocal, boolean isManifest) {
1116        return setoutUtilityCars(carList, car, isLocal, isManifest, !IS_TWO_COLUMN_TRACK);
1117    }
1118
1119    protected String setoutUtilityCars(List<Car> carList, Car car, boolean isLocal, boolean isManifest,
1120            boolean isTwoColumnTrack) {
1121        int count = countSetoutUtilityCars(carList, car, isLocal, isManifest);
1122        if (count == 0) {
1123            return null;
1124        }
1125        // list utility cars by type, track, length, and load
1126        String[] format;
1127        if (isLocal && isManifest && !isTwoColumnTrack) {
1128            format = Setup.getLocalUtilityManifestMessageFormat();
1129        } else if (isLocal && !isManifest && !isTwoColumnTrack) {
1130            format = Setup.getLocalUtilitySwitchListMessageFormat();
1131        } else if (!isLocal && !isManifest && !isTwoColumnTrack) {
1132            format = Setup.getDropUtilitySwitchListMessageFormat();
1133        } else if (!isLocal && isManifest && !isTwoColumnTrack) {
1134            format = Setup.getDropUtilityManifestMessageFormat();
1135        } else if (isManifest && isTwoColumnTrack) {
1136            format = Setup.getDropTwoColumnByTrackUtilityManifestMessageFormat();
1137        } else {
1138            format = Setup.getDropTwoColumnByTrackUtilitySwitchListMessageFormat();
1139        }
1140        StringBuffer buf = new StringBuffer(SPACE + padString(Integer.toString(count), UTILITY_CAR_COUNT_FIELD_SIZE));
1141        // TODO the Setup.Location doesn't work correctly for the conductor
1142        // window due to the fact that the car can be in the train and not
1143        // at its starting location.
1144        // Therefore we use the local true to disable it.
1145        if (car.getTrack() == null) {
1146            isLocal = true;
1147        }
1148        for (String attribute : format) {
1149            buf.append(getCarAttribute(car, attribute, !PICKUP, isLocal));
1150        }
1151        return buf.toString();
1152    }
1153
1154    public int countSetoutUtilityCars(List<Car> carList, Car car, boolean isLocal, boolean isManifest) {
1155        // list utility cars by type, track, length, and load
1156        String[] format;
1157        if (isLocal && isManifest) {
1158            format = Setup.getLocalUtilityManifestMessageFormat();
1159        } else if (isLocal && !isManifest) {
1160            format = Setup.getLocalUtilitySwitchListMessageFormat();
1161        } else if (!isLocal && !isManifest) {
1162            format = Setup.getDropUtilitySwitchListMessageFormat();
1163        } else {
1164            format = Setup.getDropUtilityManifestMessageFormat();
1165        }
1166        return countUtilityCars(format, carList, car, !PICKUP);
1167    }
1168
1169    /**
1170     * Scans the car list for utility cars that have the same attributes as the
1171     * car provided. Returns 0 if this car type has already been processed,
1172     * otherwise the number of cars with the same attribute.
1173     *
1174     * @param format   Message format.
1175     * @param carList  List of cars for this train
1176     * @param car      The utility car.
1177     * @param isPickup True if pick up, false if set out.
1178     * @return 0 if the car type has already been processed
1179     */
1180    protected int countUtilityCars(String[] format, List<Car> carList, Car car, boolean isPickup) {
1181        int count = 0;
1182        // figure out if the user wants to show the car's length
1183        boolean showLength = showUtilityCarLength(format);
1184        // figure out if the user want to show the car's loads
1185        boolean showLoad = showUtilityCarLoad(format);
1186        boolean showLocation = false;
1187        boolean showDestination = false;
1188        String carType = car.getTypeName().split(HYPHEN)[0];
1189        String carAttributes;
1190        // Note for car pick up: type, id, track name. For set out type, track
1191        // name, id (reversed).
1192        if (isPickup) {
1193            carAttributes = carType + car.getRouteLocationId() + car.getSplitTrackName();
1194            showDestination = showUtilityCarDestination(format);
1195            if (showDestination) {
1196                carAttributes = carAttributes + car.getRouteDestinationId();
1197            }
1198        } else {
1199            // set outs and local moves
1200            carAttributes = carType + car.getSplitDestinationTrackName() + car.getRouteDestinationId();
1201            showLocation = showUtilityCarLocation(format);
1202            if (showLocation && car.getTrack() != null) {
1203                carAttributes = carAttributes + car.getRouteLocationId();
1204            }
1205        }
1206        if (car.isLocalMove()) {
1207            carAttributes = carAttributes + car.getSplitTrackName();
1208        }
1209        if (showLength) {
1210            carAttributes = carAttributes + car.getLength();
1211        }
1212        if (showLoad) {
1213            carAttributes = carAttributes + car.getLoadName();
1214        }
1215        // have we already done this car type?
1216        if (!utilityCarTypes.contains(carAttributes)) {
1217            utilityCarTypes.add(carAttributes); // don't do this type again
1218            // determine how many cars of this type
1219            for (Car c : carList) {
1220                if (!c.isUtility()) {
1221                    continue;
1222                }
1223                String cType = c.getTypeName().split(HYPHEN)[0];
1224                if (!cType.equals(carType)) {
1225                    continue;
1226                }
1227                if (showLength && !c.getLength().equals(car.getLength())) {
1228                    continue;
1229                }
1230                if (showLoad && !c.getLoadName().equals(car.getLoadName())) {
1231                    continue;
1232                }
1233                if (showLocation && !c.getRouteLocationId().equals(car.getRouteLocationId())) {
1234                    continue;
1235                }
1236                if (showDestination && !c.getRouteDestinationId().equals(car.getRouteDestinationId())) {
1237                    continue;
1238                }
1239                if (car.isLocalMove() ^ c.isLocalMove()) {
1240                    continue;
1241                }
1242                if (isPickup &&
1243                        c.getRouteLocation() == car.getRouteLocation() &&
1244                        c.getSplitTrackName().equals(car.getSplitTrackName())) {
1245                    count++;
1246                }
1247                if (!isPickup &&
1248                        c.getRouteDestination() == car.getRouteDestination() &&
1249                        c.getSplitDestinationTrackName().equals(car.getSplitDestinationTrackName()) &&
1250                        (c.getSplitTrackName().equals(car.getSplitTrackName()) || !c.isLocalMove())) {
1251                    count++;
1252                }
1253            }
1254        }
1255        return count;
1256    }
1257
1258    public void clearUtilityCarTypes() {
1259        utilityCarTypes.clear();
1260    }
1261
1262    private boolean showUtilityCarLength(String[] mFormat) {
1263        return showUtilityCarAttribute(Setup.LENGTH, mFormat);
1264    }
1265
1266    private boolean showUtilityCarLoad(String[] mFormat) {
1267        return showUtilityCarAttribute(Setup.LOAD, mFormat);
1268    }
1269
1270    private boolean showUtilityCarLocation(String[] mFormat) {
1271        return showUtilityCarAttribute(Setup.LOCATION, mFormat);
1272    }
1273
1274    private boolean showUtilityCarDestination(String[] mFormat) {
1275        return showUtilityCarAttribute(Setup.DESTINATION, mFormat) ||
1276                showUtilityCarAttribute(Setup.DEST_TRACK, mFormat);
1277    }
1278
1279    private boolean showUtilityCarAttribute(String string, String[] mFormat) {
1280        for (String s : mFormat) {
1281            if (s.equals(string)) {
1282                return true;
1283            }
1284        }
1285        return false;
1286    }
1287
1288    /**
1289     * Writes a line to the build report file
1290     *
1291     * @param file   build report file
1292     * @param level  print level
1293     * @param string string to write
1294     */
1295    protected static void addLine(PrintWriter file, String level, String string) {
1296        log.debug("addLine: {}", string);
1297        if (file != null) {
1298            String[] lines = string.split(NEW_LINE);
1299            for (String line : lines) {
1300                printLine(file, level, line);
1301            }
1302        }
1303    }
1304
1305    // only used by build report
1306    private static void printLine(PrintWriter file, String level, String string) {
1307        int lineLengthMax = getLineLength(Setup.PORTRAIT, Setup.MONOSPACED, Font.PLAIN, Setup.getBuildReportFontSize());
1308        if (string.length() > lineLengthMax) {
1309            String[] words = string.split(SPACE);
1310            StringBuffer sb = new StringBuffer();
1311            for (String word : words) {
1312                if (sb.length() + word.length() < lineLengthMax) {
1313                    sb.append(word + SPACE);
1314                } else {
1315                    file.println(level + BUILD_REPORT_CHAR + SPACE + sb.toString());
1316                    sb = new StringBuffer(word + SPACE);
1317                }
1318            }
1319            string = sb.toString();
1320        }
1321        file.println(level + BUILD_REPORT_CHAR + SPACE + string);
1322    }
1323
1324    /**
1325     * Writes string to file. No line length wrap or protection.
1326     *
1327     * @param file   The File to write to.
1328     * @param string The string to write.
1329     */
1330    protected void addLine(PrintWriter file, String string) {
1331        log.debug("addLine: {}", string);
1332        if (file != null) {
1333            file.println(string);
1334        }
1335    }
1336
1337    /**
1338     * Writes a string to a file. Checks for string length, and will
1339     * automatically wrap lines.
1340     *
1341     * @param file       The File to write to.
1342     * @param string     The string to write.
1343     * @param isManifest set true for manifest page orientation, false for
1344     *                   switch list orientation
1345     */
1346    protected void newLine(PrintWriter file, String string, boolean isManifest) {
1347        String[] lines = string.split(NEW_LINE);
1348        for (String line : lines) {
1349            String[] words = line.split(SPACE);
1350            StringBuffer sb = new StringBuffer();
1351            for (String word : words) {
1352                if (checkStringLength(sb.toString() + word, isManifest)) {
1353                    sb.append(word + SPACE);
1354                } else {
1355                    sb.setLength(sb.length() - 1); // remove last space added to string
1356                    addLine(file, sb.toString());
1357                    sb = new StringBuffer(word + SPACE);
1358                }
1359            }
1360            if (sb.length() > 0) {
1361                sb.setLength(sb.length() - 1); // remove last space added to string
1362            }
1363            addLine(file, sb.toString());
1364        }
1365    }
1366
1367    /**
1368     * Adds a blank line to the file.
1369     *
1370     * @param file The File to write to.
1371     */
1372    protected void newLine(PrintWriter file) {
1373        file.println(BLANK_LINE);
1374    }
1375
1376    /**
1377     * Splits a string (example-number) as long as the second part of the string
1378     * is an integer or if the first character after the hyphen is a left
1379     * parenthesis "(".
1380     *
1381     * @param name The string to split if necessary.
1382     * @return First half of the string.
1383     */
1384    public static String splitString(String name) {
1385        String[] splitname = name.split(HYPHEN);
1386        // is the hyphen followed by a number or left parenthesis?
1387        if (splitname.length > 1 && !splitname[1].startsWith("(")) {
1388            try {
1389                Integer.parseInt(splitname[1]);
1390            } catch (NumberFormatException e) {
1391                // no return full name
1392                return name.trim();
1393            }
1394        }
1395        return splitname[0].trim();
1396    }
1397
1398    /**
1399     * Splits a string if there's a hyphen followed by a left parenthesis "-(".
1400     *
1401     * @return First half of the string.
1402     */
1403    private static String splitStringLeftParenthesis(String name) {
1404        String[] splitname = name.split(HYPHEN);
1405        if (splitname.length > 1 && splitname[1].startsWith("(")) {
1406            return splitname[0].trim();
1407        }
1408        return name.trim();
1409    }
1410
1411    // returns true if there's work at location
1412    protected boolean isThereWorkAtLocation(List<Car> carList, List<Engine> engList, RouteLocation rl) {
1413        if (carList != null) {
1414            for (Car car : carList) {
1415                if (car.getRouteLocation() == rl || car.getRouteDestination() == rl) {
1416                    return true;
1417                }
1418            }
1419        }
1420        if (engList != null) {
1421            for (Engine eng : engList) {
1422                if (eng.getRouteLocation() == rl || eng.getRouteDestination() == rl) {
1423                    return true;
1424                }
1425            }
1426        }
1427        return false;
1428    }
1429
1430    /**
1431     * returns true if the train has work at the location
1432     *
1433     * @param train    The Train.
1434     * @param location The Location.
1435     * @return true if the train has work at the location
1436     */
1437    public static boolean isThereWorkAtLocation(Train train, Location location) {
1438        if (isThereWorkAtLocation(train, location, InstanceManager.getDefault(CarManager.class).getList(train))) {
1439            return true;
1440        }
1441        if (isThereWorkAtLocation(train, location, InstanceManager.getDefault(EngineManager.class).getList(train))) {
1442            return true;
1443        }
1444        return false;
1445    }
1446
1447    private static boolean isThereWorkAtLocation(Train train, Location location, List<? extends RollingStock> list) {
1448        for (RollingStock rs : list) {
1449            if ((rs.getRouteLocation() != null &&
1450                    rs.getTrack() != null &&
1451                    rs.getRouteLocation().getSplitName()
1452                            .equals(location.getSplitName())) ||
1453                    (rs.getRouteDestination() != null &&
1454                            rs.getRouteDestination().getSplitName().equals(location.getSplitName()))) {
1455                return true;
1456            }
1457        }
1458        return false;
1459    }
1460
1461    protected void addCarsLocationUnknown(PrintWriter file, boolean isManifest) {
1462        List<Car> cars = carManager.getCarsLocationUnknown();
1463        if (cars.size() == 0) {
1464            return; // no cars to search for!
1465        }
1466        newLine(file);
1467        newLine(file, Setup.getMiaComment(), isManifest);
1468        for (Car car : cars) {
1469            addSearchForCar(file, car);
1470        }
1471    }
1472
1473    private void addSearchForCar(PrintWriter file, Car car) {
1474        StringBuffer buf = new StringBuffer();
1475        String[] format = Setup.getMissingCarMessageFormat();
1476        for (String attribute : format) {
1477            buf.append(getCarAttribute(car, attribute, false, false));
1478        }
1479        addLine(file, buf.toString());
1480    }
1481
1482    /*
1483     * Gets an engine's attribute String. Returns empty if there isn't an
1484     * attribute and not using the tabular feature. isPickup true when engine is
1485     * being picked up.
1486     */
1487    private String getEngineAttribute(Engine engine, String attribute, boolean isPickup) {
1488        if (!attribute.equals(Setup.BLANK)) {
1489            String s = SPACE + getEngineAttrib(engine, attribute, isPickup);
1490            if (Setup.isTabEnabled() || !s.trim().isEmpty()) {
1491                return s;
1492            }
1493        }
1494        return "";
1495    }
1496
1497    /*
1498     * Can not use String case statement since Setup.MODEL, etc, are not fixed
1499     * strings.
1500     */
1501    private String getEngineAttrib(Engine engine, String attribute, boolean isPickup) {
1502        if (attribute.equals(Setup.MODEL)) {
1503            return padAndTruncateIfNeeded(splitStringLeftParenthesis(engine.getModel()),
1504                    InstanceManager.getDefault(EngineModels.class).getMaxNameLength());
1505        } else if (attribute.equals(Setup.HP)) {
1506            return padAndTruncateIfNeeded(engine.getHp(), 5) +
1507                    (Setup.isPrintHeadersEnabled() ? "" : TrainManifestHeaderText.getStringHeader_Hp());
1508        } else if (attribute.equals(Setup.CONSIST)) {
1509            return padAndTruncateIfNeeded(engine.getConsistName(),
1510                    InstanceManager.getDefault(ConsistManager.class).getMaxNameLength());
1511        } else if (attribute.equals(Setup.DCC_ADDRESS)) {
1512            return padAndTruncateIfNeeded(engine.getDccAddress(),
1513                    TrainManifestHeaderText.getStringHeader_DCC_Address().length());
1514        } else if (attribute.equals(Setup.COMMENT)) {
1515            return padAndTruncateIfNeeded(engine.getComment(), engineManager.getMaxCommentLength());
1516        }
1517        return getRollingStockAttribute(engine, attribute, isPickup, false);
1518    }
1519
1520    /*
1521     * Gets a car's attribute String. Returns empty if there isn't an attribute
1522     * and not using the tabular feature. isPickup true when car is being picked
1523     * up. isLocal true when car is performing a local move.
1524     */
1525    private String getCarAttribute(Car car, String attribute, boolean isPickup, boolean isLocal) {
1526        if (!attribute.equals(Setup.BLANK)) {
1527            String s = SPACE + getCarAttrib(car, attribute, isPickup, isLocal);
1528            if (Setup.isTabEnabled() || !s.trim().isEmpty()) {
1529                return s;
1530            }
1531        }
1532        return "";
1533    }
1534
1535    private String getCarAttrib(Car car, String attribute, boolean isPickup, boolean isLocal) {
1536        if (attribute.equals(Setup.LOAD)) {
1537            return ((car.isCaboose() && !Setup.isPrintCabooseLoadEnabled()) ||
1538                    (car.isPassenger() && !Setup.isPrintPassengerLoadEnabled()))
1539                            ? padAndTruncateIfNeeded("",
1540                                    InstanceManager.getDefault(CarLoads.class).getMaxNameLength())
1541                            : padAndTruncateIfNeeded(car.getLoadName().split(HYPHEN)[0],
1542                                    InstanceManager.getDefault(CarLoads.class).getMaxNameLength());
1543        } else if (attribute.equals(Setup.LOAD_TYPE)) {
1544            return padAndTruncateIfNeeded(car.getLoadType(),
1545                    TrainManifestHeaderText.getStringHeader_Load_Type().length());
1546        } else if (attribute.equals(Setup.HAZARDOUS)) {
1547            return (car.isHazardous() ? Setup.getHazardousMsg()
1548                    : padAndTruncateIfNeeded("", Setup.getHazardousMsg().length()));
1549        } else if (attribute.equals(Setup.DROP_COMMENT)) {
1550            return padAndTruncateIfNeeded(car.getDropComment(),
1551                    InstanceManager.getDefault(CarLoads.class).getMaxLoadCommentLength());
1552        } else if (attribute.equals(Setup.PICKUP_COMMENT)) {
1553            return padAndTruncateIfNeeded(car.getPickupComment(),
1554                    InstanceManager.getDefault(CarLoads.class).getMaxLoadCommentLength());
1555        } else if (attribute.equals(Setup.KERNEL)) {
1556            return padAndTruncateIfNeeded(car.getKernelName(),
1557                    InstanceManager.getDefault(KernelManager.class).getMaxNameLength());
1558        } else if (attribute.equals(Setup.KERNEL_SIZE)) {
1559            if (car.getKernel() != null) {
1560                return padAndTruncateIfNeeded(Integer.toString(car.getKernel().getSize()), 2);
1561            } else {
1562                return SPACE + SPACE; // assumes that kernel size is 99 or less
1563            }
1564        } else if (attribute.equals(Setup.RWE)) {
1565            if (!car.getReturnWhenEmptyDestinationName().equals(Car.NONE)) {
1566                // format RWE destination and track name
1567                String rweAndTrackName = car.getSplitReturnWhenEmptyDestinationName();
1568                if (!car.getReturnWhenEmptyDestTrackName().equals(Car.NONE)) {
1569                    rweAndTrackName = rweAndTrackName + "," + SPACE + car.getSplitReturnWhenEmptyDestinationTrackName();
1570                }
1571                return Setup.isPrintHeadersEnabled()
1572                        ? padAndTruncateIfNeeded(rweAndTrackName, locationManager.getMaxLocationAndTrackNameLength())
1573                        : padAndTruncateIfNeeded(
1574                                TrainManifestHeaderText.getStringHeader_RWE() + SPACE + rweAndTrackName,
1575                                locationManager.getMaxLocationAndTrackNameLength() +
1576                                        TrainManifestHeaderText.getStringHeader_RWE().length() +
1577                                        3);
1578            }
1579            return padAndTruncateIfNeeded("", locationManager.getMaxLocationAndTrackNameLength());
1580        } else if (attribute.equals(Setup.FINAL_DEST)) {
1581            return Setup.isPrintHeadersEnabled()
1582                    ? padAndTruncateIfNeeded(car.getSplitFinalDestinationName(),
1583                            locationManager.getMaxLocationNameLength())
1584                    : padAndTruncateIfNeeded(
1585                            TrainManifestText.getStringFinalDestination() +
1586                                    SPACE +
1587                                    car.getSplitFinalDestinationName(),
1588                            locationManager.getMaxLocationNameLength() +
1589                                    TrainManifestText.getStringFinalDestination().length() +
1590                                    1);
1591        } else if (attribute.equals(Setup.FINAL_DEST_TRACK)) {
1592            // format final destination and track name
1593            String FDAndTrackName = car.getSplitFinalDestinationName();
1594            if (!car.getFinalDestinationTrackName().equals(Car.NONE)) {
1595                FDAndTrackName = FDAndTrackName + "," + SPACE + car.getSplitFinalDestinationTrackName();
1596            }
1597            return Setup.isPrintHeadersEnabled()
1598                    ? padAndTruncateIfNeeded(FDAndTrackName, locationManager.getMaxLocationAndTrackNameLength() + 2)
1599                    : padAndTruncateIfNeeded(TrainManifestText.getStringFinalDestination() + SPACE + FDAndTrackName,
1600                            locationManager.getMaxLocationAndTrackNameLength() +
1601                                    TrainManifestText.getStringFinalDestination().length() +
1602                                    3);
1603        } else if (attribute.equals(Setup.DIVISION)) {
1604            return padAndTruncateIfNeeded(car.getDivisionName(),
1605                    InstanceManager.getDefault(DivisionManager.class).getMaxDivisionNameLength());
1606        } else if (attribute.equals(Setup.COMMENT)) {
1607            return padAndTruncateIfNeeded(car.getComment(), carManager.getMaxCommentLength());
1608        }
1609        return getRollingStockAttribute(car, attribute, isPickup, isLocal);
1610    }
1611
1612    private String getRollingStockAttribute(RollingStock rs, String attribute, boolean isPickup, boolean isLocal) {
1613        try {
1614            if (attribute.equals(Setup.NUMBER)) {
1615                return padAndTruncateIfNeeded(splitString(rs.getNumber()), Control.max_len_string_print_road_number);
1616            } else if (attribute.equals(Setup.ROAD)) {
1617                String road = rs.getRoadName().split(HYPHEN)[0];
1618                return padAndTruncateIfNeeded(road, InstanceManager.getDefault(CarRoads.class).getMaxNameLength());
1619            } else if (attribute.equals(Setup.TYPE)) {
1620                String type = rs.getTypeName().split(HYPHEN)[0];
1621                return padAndTruncateIfNeeded(type, InstanceManager.getDefault(CarTypes.class).getMaxNameLength());
1622            } else if (attribute.equals(Setup.LENGTH)) {
1623                return padAndTruncateIfNeeded(rs.getLength() + Setup.getLengthUnitAbv(),
1624                        InstanceManager.getDefault(CarLengths.class).getMaxNameLength());
1625            } else if (attribute.equals(Setup.WEIGHT)) {
1626                return padAndTruncateIfNeeded(Integer.toString(rs.getAdjustedWeightTons()),
1627                        Control.max_len_string_weight_name) +
1628                        (Setup.isPrintHeadersEnabled() ? "" : TrainManifestHeaderText.getStringHeader_Weight());
1629            } else if (attribute.equals(Setup.COLOR)) {
1630                return padAndTruncateIfNeeded(rs.getColor(),
1631                        InstanceManager.getDefault(CarColors.class).getMaxNameLength());
1632            } else if (((attribute.equals(Setup.LOCATION)) && (isPickup || isLocal)) ||
1633                    (attribute.equals(Setup.TRACK) && isPickup)) {
1634                return Setup.isPrintHeadersEnabled()
1635                        ? padAndTruncateIfNeeded(rs.getSplitTrackName(),
1636                                locationManager.getMaxTrackNameLength())
1637                        : padAndTruncateIfNeeded(
1638                                TrainManifestText.getStringFrom() + SPACE + rs.getSplitTrackName(),
1639                                TrainManifestText.getStringFrom().length() +
1640                                        locationManager.getMaxTrackNameLength() +
1641                                        1);
1642            } else if (attribute.equals(Setup.LOCATION) && !isPickup && !isLocal) {
1643                return Setup.isPrintHeadersEnabled()
1644                        ? padAndTruncateIfNeeded(rs.getSplitLocationName(),
1645                                locationManager.getMaxLocationNameLength())
1646                        : padAndTruncateIfNeeded(
1647                                TrainManifestText.getStringFrom() + SPACE + rs.getSplitLocationName(),
1648                                locationManager.getMaxLocationNameLength() +
1649                                        TrainManifestText.getStringFrom().length() +
1650                                        1);
1651            } else if (attribute.equals(Setup.DESTINATION) && isPickup) {
1652                if (Setup.isPrintHeadersEnabled()) {
1653                    return padAndTruncateIfNeeded(rs.getSplitDestinationName(),
1654                            locationManager.getMaxLocationNameLength());
1655                }
1656                if (Setup.isTabEnabled()) {
1657                    return padAndTruncateIfNeeded(
1658                            TrainManifestText.getStringDest() + SPACE + rs.getSplitDestinationName(),
1659                            TrainManifestText.getStringDest().length() +
1660                                    locationManager.getMaxLocationNameLength() +
1661                                    1);
1662                } else {
1663                    return TrainManifestText.getStringDestination() +
1664                            SPACE +
1665                            rs.getSplitDestinationName();
1666                }
1667            } else if ((attribute.equals(Setup.DESTINATION) || attribute.equals(Setup.TRACK)) && !isPickup) {
1668                return Setup.isPrintHeadersEnabled()
1669                        ? padAndTruncateIfNeeded(rs.getSplitDestinationTrackName(),
1670                                locationManager.getMaxTrackNameLength())
1671                        : padAndTruncateIfNeeded(
1672                                TrainManifestText.getStringTo() +
1673                                        SPACE +
1674                                        rs.getSplitDestinationTrackName(),
1675                                locationManager.getMaxTrackNameLength() +
1676                                        TrainManifestText.getStringTo().length() +
1677                                        1);
1678            } else if (attribute.equals(Setup.DEST_TRACK)) {
1679                // format destination name and destination track name
1680                String destAndTrackName =
1681                        rs.getSplitDestinationName() + "," + SPACE + rs.getSplitDestinationTrackName();
1682                return Setup.isPrintHeadersEnabled()
1683                        ? padAndTruncateIfNeeded(destAndTrackName,
1684                                locationManager.getMaxLocationAndTrackNameLength() + 2)
1685                        : padAndTruncateIfNeeded(TrainManifestText.getStringDest() + SPACE + destAndTrackName,
1686                                locationManager.getMaxLocationAndTrackNameLength() +
1687                                        TrainManifestText.getStringDest().length() +
1688                                        3);
1689            } else if (attribute.equals(Setup.OWNER)) {
1690                return padAndTruncateIfNeeded(rs.getOwnerName(),
1691                        InstanceManager.getDefault(CarOwners.class).getMaxNameLength());
1692            } // the three utility attributes that don't get printed but need to
1693              // be tabbed out
1694            else if (attribute.equals(Setup.NO_NUMBER)) {
1695                return padAndTruncateIfNeeded("",
1696                        Control.max_len_string_print_road_number - (UTILITY_CAR_COUNT_FIELD_SIZE + 1));
1697            } else if (attribute.equals(Setup.NO_ROAD)) {
1698                return padAndTruncateIfNeeded("", InstanceManager.getDefault(CarRoads.class).getMaxNameLength());
1699            } else if (attribute.equals(Setup.NO_COLOR)) {
1700                return padAndTruncateIfNeeded("", InstanceManager.getDefault(CarColors.class).getMaxNameLength());
1701            } // there are four truncated manifest attributes
1702            else if (attribute.equals(Setup.NO_DEST_TRACK)) {
1703                return Setup.isPrintHeadersEnabled()
1704                        ? padAndTruncateIfNeeded("", locationManager.getMaxLocationAndTrackNameLength() + 1)
1705                        : "";
1706            } else if ((attribute.equals(Setup.NO_LOCATION) && !isPickup) ||
1707                    (attribute.equals(Setup.NO_DESTINATION) && isPickup)) {
1708                return Setup.isPrintHeadersEnabled()
1709                        ? padAndTruncateIfNeeded("", locationManager.getMaxLocationNameLength())
1710                        : "";
1711            } else if (attribute.equals(Setup.NO_TRACK) ||
1712                    attribute.equals(Setup.NO_LOCATION) ||
1713                    attribute.equals(Setup.NO_DESTINATION)) {
1714                return Setup.isPrintHeadersEnabled()
1715                        ? padAndTruncateIfNeeded("", locationManager.getMaxTrackNameLength())
1716                        : "";
1717            } else if (attribute.equals(Setup.TAB)) {
1718                return createTabIfNeeded(Setup.getTab1Length() - 1);
1719            } else if (attribute.equals(Setup.TAB2)) {
1720                return createTabIfNeeded(Setup.getTab2Length() - 1);
1721            } else if (attribute.equals(Setup.TAB3)) {
1722                return createTabIfNeeded(Setup.getTab3Length() - 1);
1723            }
1724            // something isn't right!
1725            return Bundle.getMessage("ErrorPrintOptions", attribute);
1726
1727        } catch (ArrayIndexOutOfBoundsException e) {
1728            if (attribute.equals(Setup.ROAD)) {
1729                return padAndTruncateIfNeeded("", InstanceManager.getDefault(CarRoads.class).getMaxNameLength());
1730            } else if (attribute.equals(Setup.TYPE)) {
1731                return padAndTruncateIfNeeded("", InstanceManager.getDefault(CarTypes.class).getMaxNameLength());
1732            }
1733            // something isn't right!
1734            return Bundle.getMessage("ErrorPrintOptions", attribute);
1735        }
1736    }
1737
1738    /**
1739     * Two column header format. Left side pick ups, right side set outs
1740     *
1741     * @param file       Manifest or switch list File.
1742     * @param isManifest True if manifest, false if switch list.
1743     */
1744    public void printEngineHeader(PrintWriter file, boolean isManifest) {
1745        int lineLength = getLineLength(isManifest);
1746        printHorizontalLine(file, 0, lineLength);
1747        if (!Setup.isPrintHeadersEnabled()) {
1748            return;
1749        }
1750        if (!Setup.getPickupEnginePrefix().trim().isEmpty() || !Setup.getDropEnginePrefix().trim().isEmpty()) {
1751            // center engine pick up and set out text
1752            String s = padAndTruncate(tabString(Setup.getPickupEnginePrefix().trim(),
1753                    lineLength / 4 - Setup.getPickupEnginePrefix().length() / 2), lineLength / 2) +
1754                    VERTICAL_LINE_CHAR +
1755                    tabString(Setup.getDropEnginePrefix(), lineLength / 4 - Setup.getDropEnginePrefix().length() / 2);
1756            s = padAndTruncate(s, lineLength);
1757            addLine(file, s);
1758            printHorizontalLine(file, 0, lineLength);
1759        }
1760
1761        String s = padAndTruncate(getPickupEngineHeader(), lineLength / 2);
1762        s = padAndTruncate(s + VERTICAL_LINE_CHAR + getDropEngineHeader(), lineLength);
1763        addLine(file, s);
1764        printHorizontalLine(file, 0, lineLength);
1765    }
1766
1767    public void printPickupEngineHeader(PrintWriter file, boolean isManifest) {
1768        int lineLength = getLineLength(isManifest);
1769        printHorizontalLine(file, 0, lineLength);
1770        String s = padAndTruncate(createTabIfNeeded(Setup.getManifestPrefixLength() + 1) + getPickupEngineHeader(),
1771                lineLength);
1772        addLine(file, s);
1773        printHorizontalLine(file, 0, lineLength);
1774    }
1775
1776    public void printDropEngineHeader(PrintWriter file, boolean isManifest) {
1777        int lineLength = getLineLength(isManifest);
1778        printHorizontalLine(file, 0, lineLength);
1779        String s = padAndTruncate(createTabIfNeeded(Setup.getManifestPrefixLength() + 1) + getDropEngineHeader(),
1780                lineLength);
1781        addLine(file, s);
1782        printHorizontalLine(file, 0, lineLength);
1783    }
1784
1785    /**
1786     * Prints the two column header for cars. Left side pick ups, right side set
1787     * outs.
1788     *
1789     * @param file             Manifest or Switch List File
1790     * @param isManifest       True if manifest, false if switch list.
1791     * @param isTwoColumnTrack True if two column format using track names.
1792     */
1793    public void printCarHeader(PrintWriter file, boolean isManifest, boolean isTwoColumnTrack) {
1794        int lineLength = getLineLength(isManifest);
1795        printHorizontalLine(file, 0, lineLength);
1796        if (!Setup.isPrintHeadersEnabled()) {
1797            return;
1798        }
1799        // center pick up and set out text
1800        String s = padAndTruncate(
1801                tabString(Setup.getPickupCarPrefix(), lineLength / 4 - Setup.getPickupCarPrefix().length() / 2),
1802                lineLength / 2) +
1803                VERTICAL_LINE_CHAR +
1804                tabString(Setup.getDropCarPrefix(), lineLength / 4 - Setup.getDropCarPrefix().length() / 2);
1805        s = padAndTruncate(s, lineLength);
1806        addLine(file, s);
1807        printHorizontalLine(file, 0, lineLength);
1808
1809        s = padAndTruncate(getPickupCarHeader(isManifest, isTwoColumnTrack), lineLength / 2);
1810        s = padAndTruncate(s + VERTICAL_LINE_CHAR + getDropCarHeader(isManifest, isTwoColumnTrack), lineLength);
1811        addLine(file, s);
1812        printHorizontalLine(file, 0, lineLength);
1813    }
1814
1815    public void printPickupCarHeader(PrintWriter file, boolean isManifest, boolean isTwoColumnTrack) {
1816        if (!Setup.isPrintHeadersEnabled()) {
1817            return;
1818        }
1819        printHorizontalLine(file, isManifest);
1820        String s = padAndTruncate(createTabIfNeeded(Setup.getManifestPrefixLength() + 1) +
1821                getPickupCarHeader(isManifest, isTwoColumnTrack), getLineLength(isManifest));
1822        addLine(file, s);
1823        printHorizontalLine(file, isManifest);
1824    }
1825
1826    public void printDropCarHeader(PrintWriter file, boolean isManifest, boolean isTwoColumnTrack) {
1827        if (!Setup.isPrintHeadersEnabled() || getDropCarHeader(isManifest, isTwoColumnTrack).trim().isEmpty()) {
1828            return;
1829        }
1830        printHorizontalLine(file, isManifest);
1831        String s = padAndTruncate(
1832                createTabIfNeeded(Setup.getManifestPrefixLength() + 1) + getDropCarHeader(isManifest, isTwoColumnTrack),
1833                getLineLength(isManifest));
1834        addLine(file, s);
1835        printHorizontalLine(file, isManifest);
1836    }
1837
1838    public void printLocalCarMoveHeader(PrintWriter file, boolean isManifest) {
1839        if (!Setup.isPrintHeadersEnabled()) {
1840            return;
1841        }
1842        printHorizontalLine(file, isManifest);
1843        String s = padAndTruncate(
1844                createTabIfNeeded(Setup.getManifestPrefixLength() + 1) + getLocalMoveHeader(isManifest),
1845                getLineLength(isManifest));
1846        addLine(file, s);
1847        printHorizontalLine(file, isManifest);
1848    }
1849
1850    public String getPickupEngineHeader() {
1851        return getHeader(Setup.getPickupEngineMessageFormat(), PICKUP, !LOCAL, ENGINE);
1852    }
1853
1854    public String getDropEngineHeader() {
1855        return getHeader(Setup.getDropEngineMessageFormat(), !PICKUP, !LOCAL, ENGINE);
1856    }
1857
1858    public String getPickupCarHeader(boolean isManifest, boolean isTwoColumnTrack) {
1859        if (isManifest && !isTwoColumnTrack) {
1860            return getHeader(Setup.getPickupManifestMessageFormat(), PICKUP, !LOCAL, !ENGINE);
1861        } else if (!isManifest && !isTwoColumnTrack) {
1862            return getHeader(Setup.getPickupSwitchListMessageFormat(), PICKUP, !LOCAL, !ENGINE);
1863        } else if (isManifest && isTwoColumnTrack) {
1864            return getHeader(Setup.getPickupTwoColumnByTrackManifestMessageFormat(), PICKUP, !LOCAL, !ENGINE);
1865        } else {
1866            return getHeader(Setup.getPickupTwoColumnByTrackSwitchListMessageFormat(), PICKUP, !LOCAL, !ENGINE);
1867        }
1868    }
1869
1870    public String getDropCarHeader(boolean isManifest, boolean isTwoColumnTrack) {
1871        if (isManifest && !isTwoColumnTrack) {
1872            return getHeader(Setup.getDropManifestMessageFormat(), !PICKUP, !LOCAL, !ENGINE);
1873        } else if (!isManifest && !isTwoColumnTrack) {
1874            return getHeader(Setup.getDropSwitchListMessageFormat(), !PICKUP, !LOCAL, !ENGINE);
1875        } else if (isManifest && isTwoColumnTrack) {
1876            return getHeader(Setup.getDropTwoColumnByTrackManifestMessageFormat(), !PICKUP, !LOCAL, !ENGINE);
1877        } else {
1878            return getHeader(Setup.getDropTwoColumnByTrackSwitchListMessageFormat(), !PICKUP, !LOCAL, !ENGINE);
1879        }
1880    }
1881
1882    public String getLocalMoveHeader(boolean isManifest) {
1883        if (isManifest) {
1884            return getHeader(Setup.getLocalManifestMessageFormat(), !PICKUP, LOCAL, !ENGINE);
1885        } else {
1886            return getHeader(Setup.getLocalSwitchListMessageFormat(), !PICKUP, LOCAL, !ENGINE);
1887        }
1888    }
1889
1890    private String getHeader(String[] format, boolean isPickup, boolean isLocal, boolean isEngine) {
1891        StringBuffer buf = new StringBuffer();
1892        for (String attribute : format) {
1893            if (attribute.equals(Setup.BLANK)) {
1894                continue;
1895            }
1896            if (attribute.equals(Setup.ROAD)) {
1897                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Road(),
1898                        InstanceManager.getDefault(CarRoads.class).getMaxNameLength()) + SPACE);
1899            } else if (attribute.equals(Setup.NUMBER) && !isEngine) {
1900                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Number(),
1901                        Control.max_len_string_print_road_number) + SPACE);
1902            } else if (attribute.equals(Setup.NUMBER) && isEngine) {
1903                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_EngineNumber(),
1904                        Control.max_len_string_print_road_number) + SPACE);
1905            } else if (attribute.equals(Setup.TYPE)) {
1906                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Type(),
1907                        InstanceManager.getDefault(CarTypes.class).getMaxNameLength()) + SPACE);
1908            } else if (attribute.equals(Setup.MODEL)) {
1909                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Model(),
1910                        InstanceManager.getDefault(EngineModels.class).getMaxNameLength()) + SPACE);
1911            } else if (attribute.equals(Setup.HP)) {
1912                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Hp(),
1913                        5) + SPACE);
1914            } else if (attribute.equals(Setup.CONSIST)) {
1915                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Consist(),
1916                        InstanceManager.getDefault(ConsistManager.class).getMaxNameLength()) + SPACE);
1917            } else if (attribute.equals(Setup.DCC_ADDRESS)) {
1918                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_DCC_Address(),
1919                        TrainManifestHeaderText.getStringHeader_DCC_Address().length()) + SPACE);
1920            } else if (attribute.equals(Setup.KERNEL)) {
1921                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Kernel(),
1922                        InstanceManager.getDefault(KernelManager.class).getMaxNameLength()) + SPACE);
1923            } else if (attribute.equals(Setup.KERNEL_SIZE)) {
1924                buf.append("   "); // assume kernel size is 99 or less
1925            } else if (attribute.equals(Setup.LOAD)) {
1926                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Load(),
1927                        InstanceManager.getDefault(CarLoads.class).getMaxNameLength()) + SPACE);
1928            } else if (attribute.equals(Setup.LOAD_TYPE)) {
1929                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Load_Type(),
1930                        TrainManifestHeaderText.getStringHeader_Load_Type().length()) + SPACE);
1931            } else if (attribute.equals(Setup.COLOR)) {
1932                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Color(),
1933                        InstanceManager.getDefault(CarColors.class).getMaxNameLength()) + SPACE);
1934            } else if (attribute.equals(Setup.OWNER)) {
1935                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Owner(),
1936                        InstanceManager.getDefault(CarOwners.class).getMaxNameLength()) + SPACE);
1937            } else if (attribute.equals(Setup.LENGTH)) {
1938                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Length(),
1939                        InstanceManager.getDefault(CarLengths.class).getMaxNameLength()) + SPACE);
1940            } else if (attribute.equals(Setup.WEIGHT)) {
1941                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Weight(),
1942                        Control.max_len_string_weight_name) + SPACE);
1943            } else if (attribute.equals(Setup.TRACK)) {
1944                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Track(),
1945                        locationManager.getMaxTrackNameLength()) + SPACE);
1946            } else if (attribute.equals(Setup.LOCATION) && (isPickup || isLocal)) {
1947                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Location(),
1948                        locationManager.getMaxTrackNameLength()) + SPACE);
1949            } else if (attribute.equals(Setup.LOCATION) && !isPickup) {
1950                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Location(),
1951                        locationManager.getMaxLocationNameLength()) + SPACE);
1952            } else if (attribute.equals(Setup.DESTINATION) && !isPickup) {
1953                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Destination(),
1954                        locationManager.getMaxTrackNameLength()) + SPACE);
1955            } else if (attribute.equals(Setup.DESTINATION) && isPickup) {
1956                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Destination(),
1957                        locationManager.getMaxLocationNameLength()) + SPACE);
1958            } else if (attribute.equals(Setup.DEST_TRACK)) {
1959                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Dest_Track(),
1960                        locationManager.getMaxLocationAndTrackNameLength() + 2) + SPACE);
1961            } else if (attribute.equals(Setup.FINAL_DEST)) {
1962                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Final_Dest(),
1963                        locationManager.getMaxLocationNameLength()) + SPACE);
1964            } else if (attribute.equals(Setup.FINAL_DEST_TRACK)) {
1965                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Final_Dest_Track(),
1966                        locationManager.getMaxLocationAndTrackNameLength() + 2) + SPACE);
1967            } else if (attribute.equals(Setup.HAZARDOUS)) {
1968                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Hazardous(),
1969                        Setup.getHazardousMsg().length()) + SPACE);
1970            } else if (attribute.equals(Setup.RWE)) {
1971                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_RWE(),
1972                        locationManager.getMaxLocationAndTrackNameLength()) + SPACE);
1973            } else if (attribute.equals(Setup.COMMENT)) {
1974                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Comment(),
1975                        isEngine ? engineManager.getMaxCommentLength() : carManager.getMaxCommentLength()) + SPACE);
1976            } else if (attribute.equals(Setup.DROP_COMMENT)) {
1977                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Drop_Comment(),
1978                        InstanceManager.getDefault(CarLoads.class).getMaxLoadCommentLength()) + SPACE);
1979            } else if (attribute.equals(Setup.PICKUP_COMMENT)) {
1980                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Pickup_Comment(),
1981                        InstanceManager.getDefault(CarLoads.class).getMaxLoadCommentLength()) + SPACE);
1982            } else if (attribute.equals(Setup.DIVISION)) {
1983                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Division(),
1984                        InstanceManager.getDefault(DivisionManager.class).getMaxDivisionNameLength()) + SPACE);
1985            } else if (attribute.equals(Setup.TAB)) {
1986                buf.append(createTabIfNeeded(Setup.getTab1Length()));
1987            } else if (attribute.equals(Setup.TAB2)) {
1988                buf.append(createTabIfNeeded(Setup.getTab2Length()));
1989            } else if (attribute.equals(Setup.TAB3)) {
1990                buf.append(createTabIfNeeded(Setup.getTab3Length()));
1991            } else {
1992                buf.append(attribute + SPACE);
1993            }
1994        }
1995        return buf.toString().trim();
1996    }
1997
1998    protected void printTrackNameHeader(PrintWriter file, String trackName, boolean isManifest) {
1999        printHorizontalLine(file, isManifest);
2000        int lineLength = getLineLength(isManifest);
2001        String s = padAndTruncate(tabString(trackName.trim(), lineLength / 4 - trackName.trim().length() / 2),
2002                lineLength / 2) +
2003                VERTICAL_LINE_CHAR +
2004                tabString(trackName.trim(), lineLength / 4 - trackName.trim().length() / 2);
2005        s = padAndTruncate(s, lineLength);
2006        addLine(file, s);
2007        printHorizontalLine(file, isManifest);
2008    }
2009
2010    /**
2011     * Prints a line across the entire page.
2012     *
2013     * @param file       The File to print to.
2014     * @param isManifest True if manifest, false if switch list.
2015     */
2016    public void printHorizontalLine(PrintWriter file, boolean isManifest) {
2017        printHorizontalLine(file, 0, getLineLength(isManifest));
2018    }
2019
2020    public void printHorizontalLine(PrintWriter file, int start, int end) {
2021        StringBuffer sb = new StringBuffer();
2022        while (start-- > 0) {
2023            sb.append(SPACE);
2024        }
2025        while (end-- > 0) {
2026            sb.append(HORIZONTAL_LINE_CHAR);
2027        }
2028        addLine(file, sb.toString());
2029    }
2030
2031    public static String getISO8601Date(boolean isModelYear) {
2032        Calendar calendar = Calendar.getInstance();
2033        // use the JMRI Timebase (which may be a fast clock).
2034        calendar.setTime(jmri.InstanceManager.getDefault(jmri.Timebase.class).getTime());
2035        if (isModelYear && !Setup.getYearModeled().isEmpty()) {
2036            try {
2037                calendar.set(Calendar.YEAR, Integer.parseInt(Setup.getYearModeled().trim()));
2038            } catch (NumberFormatException e) {
2039                return Setup.getYearModeled();
2040            }
2041        }
2042        return (new StdDateFormat()).format(calendar.getTime());
2043    }
2044
2045    public static String getDate(Date date) {
2046        SimpleDateFormat format = new SimpleDateFormat("M/dd/yyyy HH:mm"); // NOI18N
2047        if (Setup.is12hrFormatEnabled()) {
2048            format = new SimpleDateFormat("M/dd/yyyy hh:mm a"); // NOI18N
2049        }
2050        return format.format(date);
2051    }
2052
2053    public static String getDate(boolean isModelYear) {
2054        Calendar calendar = Calendar.getInstance();
2055        // use the JMRI Timebase (which may be a fast clock).
2056        calendar.setTime(jmri.InstanceManager.getDefault(jmri.Timebase.class).getTime());
2057        if (isModelYear && !Setup.getYearModeled().equals(Setup.NONE)) {
2058            try {
2059                calendar.set(Calendar.YEAR, Integer.parseInt(Setup.getYearModeled().trim()));
2060            } catch (NumberFormatException e) {
2061                return Setup.getYearModeled();
2062            }
2063        }
2064        return TrainCommon.getDate(calendar.getTime());
2065    }
2066
2067    public static Date convertStringToDate(String date) {
2068        if (!date.isBlank()) {
2069            // create a date object from the string.
2070            try {
2071                // try MM/dd/yyyy HH:mm:ss.
2072                SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss"); // NOI18N
2073                return formatter.parse(date);
2074            } catch (java.text.ParseException pe1) {
2075                // try the old 12 hour format (no seconds).
2076                try {
2077                    SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy hh:mmaa"); // NOI18N
2078                    return formatter.parse(date);
2079                } catch (java.text.ParseException pe2) {
2080                    try {
2081                        // try 24hour clock.
2082                        SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy HH:mm"); // NOI18N
2083                        return formatter.parse(date);
2084                    } catch (java.text.ParseException pe3) {
2085                        log.debug("Not able to parse date: {}", date);
2086                    }
2087                }
2088            }
2089        }
2090        return null; // there was no date specified.
2091    }
2092
2093    /**
2094     * Pads out a string by adding spaces to the end of the string, and will
2095     * remove characters from the end of the string if the string exceeds the
2096     * field size.
2097     *
2098     * @param s         The string to pad.
2099     * @param fieldSize The maximum length of the string.
2100     * @return A String the specified length
2101     */
2102    public static String padAndTruncateIfNeeded(String s, int fieldSize) {
2103        if (Setup.isTabEnabled()) {
2104            return padAndTruncate(s, fieldSize);
2105        }
2106        return s;
2107    }
2108
2109    public static String padAndTruncate(String s, int fieldSize) {
2110        s = padString(s, fieldSize);
2111        if (s.length() > fieldSize) {
2112            s = s.substring(0, fieldSize);
2113        }
2114        return s;
2115    }
2116
2117    /**
2118     * Adjusts string to be a certain number of characters by adding spaces to
2119     * the end of the string.
2120     *
2121     * @param s         The string to pad
2122     * @param fieldSize The fixed length of the string.
2123     * @return A String the specified length
2124     */
2125    public static String padString(String s, int fieldSize) {
2126        StringBuffer buf = new StringBuffer(s);
2127        while (buf.length() < fieldSize) {
2128            buf.append(SPACE);
2129        }
2130        return buf.toString();
2131    }
2132
2133    /**
2134     * Creates a String of spaces to create a tab for text. Tabs must be
2135     * enabled. Setup.isTabEnabled()
2136     * 
2137     * @param tabSize the length of tab
2138     * @return tab
2139     */
2140    public static String createTabIfNeeded(int tabSize) {
2141        if (Setup.isTabEnabled()) {
2142            return tabString("", tabSize);
2143        }
2144        return "";
2145    }
2146
2147    protected static String tabString(String s, int tabSize) {
2148        StringBuffer buf = new StringBuffer();
2149        // TODO this doesn't consider the length of s string.
2150        while (buf.length() < tabSize) {
2151            buf.append(SPACE);
2152        }
2153        buf.append(s);
2154        return buf.toString();
2155    }
2156
2157    /**
2158     * Returns the line length for manifest or switch list printout. Always an
2159     * even number.
2160     * 
2161     * @param isManifest True if manifest.
2162     * @return line length for manifest or switch list.
2163     */
2164    public static int getLineLength(boolean isManifest) {
2165        return getLineLength(isManifest ? Setup.getManifestOrientation() : Setup.getSwitchListOrientation(),
2166                Setup.getFontName(), Font.PLAIN, Setup.getManifestFontSize());
2167    }
2168
2169    public static int getManifestHeaderLineLength() {
2170        return getLineLength(Setup.getManifestOrientation(), "SansSerif", Font.ITALIC, Setup.getManifestFontSize());
2171    }
2172
2173    private static int getLineLength(String orientation, String fontName, int fontStyle, int fontSize) {
2174        Font font = new Font(fontName, fontStyle, fontSize); // NOI18N
2175        JLabel label = new JLabel();
2176        FontMetrics metrics = label.getFontMetrics(font);
2177        int charwidth = metrics.charWidth('m');
2178        if (charwidth == 0) {
2179            log.error("Line length charater width equal to zero. font size: {}, fontName: {}", fontSize, fontName);
2180            charwidth = fontSize / 2; // create a reasonable character width
2181        }
2182        // compute lines and columns within margins
2183        int charLength = getPageSize(orientation).width / charwidth;
2184        if (charLength % 2 != 0) {
2185            charLength--; // make it even
2186        }
2187        return charLength;
2188    }
2189
2190    private boolean checkStringLength(String string, boolean isManifest) {
2191        return checkStringLength(string, isManifest ? Setup.getManifestOrientation() : Setup.getSwitchListOrientation(),
2192                Setup.getFontName(), Setup.getManifestFontSize());
2193    }
2194
2195    /**
2196     * Checks to see if the string fits on the page.
2197     *
2198     * @return false if string length is longer than page width.
2199     */
2200    private boolean checkStringLength(String string, String orientation, String fontName, int fontSize) {
2201        // ignore text color controls when determining line length
2202        if (string.startsWith(TEXT_COLOR_START) && string.contains(TEXT_COLOR_DONE)) {
2203            string = string.substring(string.indexOf(TEXT_COLOR_DONE) + 2);
2204        }
2205        if (string.contains(TEXT_COLOR_END)) {
2206            string = string.substring(0, string.indexOf(TEXT_COLOR_END));
2207        }
2208        Font font = new Font(fontName, Font.PLAIN, fontSize); // NOI18N
2209        JLabel label = new JLabel();
2210        FontMetrics metrics = label.getFontMetrics(font);
2211        int stringWidth = metrics.stringWidth(string);
2212        return stringWidth <= getPageSize(orientation).width;
2213    }
2214
2215    protected static final Dimension PAPER_MARGINS = new Dimension(84, 72);
2216
2217    protected static Dimension getPageSize(String orientation) {
2218        // page size has been adjusted to account for margins of .5
2219        // Dimension(84, 72)
2220        Dimension pagesize = new Dimension(523, 720); // Portrait 8.5 x 11
2221        // landscape has .65 margins
2222        if (orientation.equals(Setup.LANDSCAPE)) {
2223            pagesize = new Dimension(702, 523); // 11 x 8.5
2224        }
2225        if (orientation.equals(Setup.HALFPAGE)) {
2226            pagesize = new Dimension(261, 720); // 4.25 x 11
2227        }
2228        if (orientation.equals(Setup.HANDHELD)) {
2229            pagesize = new Dimension(206, 720); // 3.25 x 11
2230        }
2231        return pagesize;
2232    }
2233
2234    /**
2235     * Produces a string using commas and spaces between the strings provided in
2236     * the array. Does not check for embedded commas in the string array.
2237     *
2238     * @param array The string array to be formated.
2239     * @return formated string using commas and spaces
2240     */
2241    public static String formatStringToCommaSeparated(String[] array) {
2242        StringBuffer sbuf = new StringBuffer("");
2243        for (String s : array) {
2244            if (s != null) {
2245                sbuf = sbuf.append(s + "," + SPACE);
2246            }
2247        }
2248        if (sbuf.length() > 2) {
2249            sbuf.setLength(sbuf.length() - 2); // remove trailing separators
2250        }
2251        return sbuf.toString();
2252    }
2253
2254    private void addLine(PrintWriter file, StringBuffer buf, Color color) {
2255        String s = buf.toString();
2256        if (!s.trim().isEmpty()) {
2257            addLine(file, formatColorString(s, color));
2258        }
2259    }
2260
2261    /**
2262     * Adds HTML like color text control characters around a string. Note that
2263     * black is the standard text color, and if black is requested no control
2264     * characters are added.
2265     * 
2266     * @param text  the text to be modified
2267     * @param color the color the text is to be printed
2268     * @return formated text with color modifiers
2269     */
2270    public static String formatColorString(String text, Color color) {
2271        String s = text;
2272        if (!color.equals(Color.black)) {
2273            s = TEXT_COLOR_START + ColorUtil.colorToColorName(color) + TEXT_COLOR_DONE + text + TEXT_COLOR_END;
2274        }
2275        return s;
2276    }
2277
2278    /**
2279     * Removes the color text control characters around the desired string
2280     * 
2281     * @param string the string with control characters
2282     * @return pure text
2283     */
2284    public static String getTextColorString(String string) {
2285        String text = string;
2286        if (string.contains(TEXT_COLOR_START)) {
2287            text = string.substring(0, string.indexOf(TEXT_COLOR_START)) +
2288                    string.substring(string.indexOf(TEXT_COLOR_DONE) + 2);
2289        }
2290        if (text.contains(TEXT_COLOR_END)) {
2291            text = text.substring(0, text.indexOf(TEXT_COLOR_END)) +
2292                    string.substring(string.indexOf(TEXT_COLOR_END) + TEXT_COLOR_END.length());
2293        }
2294        return text;
2295    }
2296
2297    public static Color getTextColor(String string) {
2298        Color color = Color.black;
2299        if (string.contains(TEXT_COLOR_START)) {
2300            String c = string.substring(string.indexOf(TEXT_COLOR_START) + TEXT_COLOR_START.length());
2301            c = c.substring(0, c.indexOf("\""));
2302            color = ColorUtil.stringToColor(c);
2303        }
2304        return color;
2305    }
2306
2307    public static String getTextColorName(String string) {
2308        return ColorUtil.colorToColorName(getTextColor(string));
2309    }
2310
2311    private static final Logger log = LoggerFactory.getLogger(TrainCommon.class);
2312}