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