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