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