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