001package jmri.web.servlet.operations;
002
003import java.io.IOException;
004import java.util.*;
005
006import org.apache.commons.text.StringEscapeUtils;
007import org.slf4j.Logger;
008import org.slf4j.LoggerFactory;
009
010import jmri.InstanceManager;
011import jmri.jmrit.operations.locations.Track;
012import jmri.jmrit.operations.rollingstock.cars.Car;
013import jmri.jmrit.operations.rollingstock.cars.CarManager;
014import jmri.jmrit.operations.rollingstock.engines.Engine;
015import jmri.jmrit.operations.rollingstock.engines.EngineManager;
016import jmri.jmrit.operations.routes.RouteLocation;
017import jmri.jmrit.operations.setup.Setup;
018import jmri.jmrit.operations.trains.Train;
019import jmri.jmrit.operations.trains.TrainCommon;
020import jmri.util.FileUtil;
021
022/**
023 *
024 * @author Randall Wood
025 */
026public class HtmlConductor extends HtmlTrainCommon {
027
028    private final static Logger log = LoggerFactory.getLogger(HtmlConductor.class);
029
030    public HtmlConductor(Locale locale, Train train) throws IOException {
031        super(locale, train);
032        this.resourcePrefix = "Conductor";  // NOI18N
033    }
034
035    public String getLocation() throws IOException {
036        RouteLocation location = train.getCurrentRouteLocation();
037        if (location == null) {
038            return String.format(locale, 
039                    FileUtil.readURL(FileUtil.findURL(Bundle.getMessage(locale,"ConductorSnippet.html"))), 
040                    train.getIconName(), 
041                    StringEscapeUtils.escapeHtml4(train.getDescription()), 
042                    convertToHTMLColor(
043                            StringEscapeUtils.escapeHtml4(train.getCommentWithColor())),
044                    Setup.isPrintRouteCommentsEnabled() ? train.getRoute().getComment() : "", 
045                    strings.getProperty("Terminated"), 
046                    "", // terminated train has nothing to do // NOI18N
047                    "", // engines in separate section
048                    "", // pickup=true, local=false
049                    "", // pickup=false, local=false
050                    "", // pickup=false, local=true
051                    "", // engines in separate section
052                    "", // terminate with null string, use empty string to indicate terminated
053                    strings.getProperty("Terminated"),  // NOI18N
054                    train.getStatusCode());
055        }
056
057        List<Engine> engineList = InstanceManager.getDefault(EngineManager.class).getByTrainBlockingList(train);
058        List<Car> carList = InstanceManager.getDefault(CarManager.class).getByTrainDestinationList(train);
059        log.debug("Train has {} cars assigned to it", carList.size());
060
061        String pickups = performWork(true, false); // pickup=true, local=false
062        String setouts = performWork(false, false); // pickup=false, local=false
063        String localMoves = performWork(false, true); // pickup=false, local=true
064
065        return String.format(locale, 
066                FileUtil.readURL(FileUtil.findURL(Bundle.getMessage(locale,"ConductorSnippet.html"))), 
067                train.getIconName(), 
068                StringEscapeUtils.escapeHtml4(train.getDescription()),
069                convertToHTMLColor(StringEscapeUtils.escapeHtml4(train.getCommentWithColor())),
070                Setup.isPrintRouteCommentsEnabled() ? train.getRoute().getComment() : "", 
071                getCurrentAndNextLocation(),
072                convertToHTMLColor(getLocationComments()),
073                pickupEngines(engineList, location), // engines in separate section
074                pickups, setouts, localMoves,
075                dropEngines(engineList, location), // engines in separate section
076                (train.getNextRouteLocation(train.getCurrentRouteLocation()) != null) ? train.getNextLocationName() : null,
077                getMoveButton(),
078                train.getStatusCode());
079    }
080
081    private String getCurrentAndNextLocation() {
082        if (train.getCurrentRouteLocation() != null && train.getNextRouteLocation(train.getCurrentRouteLocation()) != null) {
083            return String.format(locale, strings.getProperty("CurrentAndNextLocation"), // NOI18N
084                    StringEscapeUtils.escapeHtml4(splitString(train.getCurrentLocationName())),
085                    StringEscapeUtils.escapeHtml4(splitString(train.getNextLocationName())));
086        } else if (train.getCurrentRouteLocation() != null) {
087            return StringEscapeUtils.escapeHtml4(splitString(train.getCurrentLocationName()));
088        }
089        return strings.getProperty("Terminated"); // NOI18N
090    }
091
092    private String getMoveButton() {
093        if (train.getNextRouteLocation(train.getCurrentRouteLocation()) != null) {
094            return String.format(locale, strings.getProperty("MoveTo"), // NOI18N
095                    StringEscapeUtils.escapeHtml4(splitString(train.getNextLocationName())));
096        } else if (train.getCurrentRouteLocation() != null) {
097            return strings.getProperty("Terminate");  // NOI18N
098        }
099        return strings.getProperty("Terminated");  // NOI18N
100    }
101
102    // needed for location comments, not yet in formatter
103    private String getEngineChanges(RouteLocation rl) {
104        // engine change or helper service?
105        if (train.getSecondLegOptions() != Train.NO_CABOOSE_OR_FRED) {
106            if (rl == train.getSecondLegStartRouteLocation()) {
107                return engineChange(rl, train.getSecondLegOptions());
108            }
109            if (rl == train.getSecondLegEndRouteLocation() && train.getSecondLegOptions() == Train.HELPER_ENGINES) {
110                return String.format(strings.getProperty("RemoveHelpersAt"), rl.getSplitName()); // NOI18N
111            }
112        }
113        if (train.getThirdLegOptions() != Train.NO_CABOOSE_OR_FRED) {
114            if (rl == train.getThirdLegStartRouteLocation()) {
115                return engineChange(rl, train.getSecondLegOptions());
116            }
117            if (rl == train.getThirdLegEndRouteLocation() && train.getThirdLegOptions() == Train.HELPER_ENGINES) {
118                return String.format(strings.getProperty("RemoveHelpersAt"), rl.getSplitName()); // NOI18N
119            }
120        }
121        return "";
122    }
123
124    private String getLocationComments() {
125        List<Car> carList = InstanceManager.getDefault(CarManager.class).getByTrainDestinationList(train);
126        StringBuilder builder = new StringBuilder();
127        RouteLocation routeLocation = train.getCurrentRouteLocation();
128        boolean work = isThereWorkAtLocation(train, routeLocation.getLocation());
129
130        // print info only if new location
131        String routeLocationName = StringEscapeUtils.escapeHtml4(routeLocation.getSplitName());
132        if (work) {
133            if (!train.isShowArrivalAndDepartureTimesEnabled()) {
134                builder.append(String.format(locale, strings.getProperty("ScheduledWorkAt"), routeLocationName)); // NOI18N
135            } else if (routeLocation == train.getTrainDepartsRouteLocation()) {
136                builder.append(String.format(locale, strings.getProperty("WorkDepartureTime"), routeLocationName, train  // NOI18N
137                        .getFormatedDepartureTime())); // NOI18N
138            } else if (!routeLocation.getDepartureTime().equals("")) {
139                builder.append(String.format(locale, strings.getProperty("WorkDepartureTime"), routeLocationName,  // NOI18N
140                        routeLocation.getFormatedDepartureTime())); // NOI18N
141            } else if (Setup.isUseDepartureTimeEnabled()
142                    && routeLocation != train.getTrainTerminatesRouteLocation()
143                    && !train.getExpectedDepartureTime(routeLocation).equals(Train.ALREADY_SERVICED)) {
144                builder.append(String.format(locale, strings.getProperty("WorkDepartureTime"), routeLocationName, train  // NOI18N
145                        .getExpectedDepartureTime(routeLocation)));
146            } else if (!train.getExpectedArrivalTime(routeLocation).equals(Train.ALREADY_SERVICED)) {
147                builder.append(String.format(locale, strings.getProperty("WorkArrivalTime"), routeLocationName, train  // NOI18N
148                        .getExpectedArrivalTime(routeLocation))); // NOI18N
149            } else {
150                builder.append(String.format(locale, strings.getProperty("ScheduledWorkAt"), routeLocationName)); // NOI18N
151            }
152            // add route comment
153            if (!routeLocation.getComment().isBlank()) {
154                builder.append(String.format(locale, strings.getProperty("RouteLocationComment"), StringEscapeUtils  // NOI18N
155                        .escapeHtml4(routeLocation.getCommentWithColor())));
156            }
157
158            builder.append(getTrackComments(routeLocation, carList));
159
160            // add location comment
161            if (Setup.isPrintLocationCommentsEnabled() && !routeLocation.getLocation().getComment().isEmpty()) {
162                builder.append(String.format(locale, strings.getProperty("LocationComment"), StringEscapeUtils  // NOI18N
163                        .escapeHtml4(routeLocation.getLocation().getCommentWithColor())));
164            }
165        }
166
167        // engine change or helper service?
168        builder.append(this.getEngineChanges(routeLocation));
169
170        if (routeLocation != train.getTrainTerminatesRouteLocation()) {
171            if (work) {
172                if (!Setup.isPrintLoadsAndEmptiesEnabled()) {
173                    // Message format: Train departs Boston Westbound with 12 cars, 450 feet, 3000 tons
174                    builder.append(String.format(strings.getProperty("TrainDepartsCars"), routeLocationName,  // NOI18N
175                            routeLocation.getTrainDirectionString(), train.getTrainLength(routeLocation), Setup
176                            .getLengthUnit().toLowerCase(), train.getTrainWeight(routeLocation), train
177                            .getNumberCarsInTrain(routeLocation)));
178                } else {
179                    // Message format: Train departs Boston Westbound with 4 loads, 8 empties, 450 feet, 3000 tons
180                    int emptyCars = train.getNumberEmptyCarsInTrain(routeLocation);
181                    builder.append(String.format(strings.getProperty("TrainDepartsLoads"), routeLocationName,  // NOI18N
182                            routeLocation.getTrainDirectionString(), train.getTrainLength(routeLocation), Setup
183                            .getLengthUnit().toLowerCase(), train.getTrainWeight(routeLocation), train
184                            .getNumberCarsInTrain(routeLocation)
185                            - emptyCars, emptyCars));
186                }
187            } else {
188                log.debug("No work ({})", routeLocation.getComment());               
189                if (routeLocation.getComment().isBlank()) {
190                    // no route comment, no work at this location
191                    if (train.isShowArrivalAndDepartureTimesEnabled()) {
192                        if (routeLocation == train.getTrainDepartsRouteLocation()) {
193                            builder.append(String.format(locale, strings
194                                    .getProperty("NoScheduledWorkAtWithDepartureTime"), routeLocationName, train  // NOI18N
195                                    .getFormatedDepartureTime()));
196                        } else if (!routeLocation.getDepartureTime().isEmpty()) {
197                            builder.append(String.format(locale, strings
198                                    .getProperty("NoScheduledWorkAtWithDepartureTime"), routeLocationName,  // NOI18N
199                                    routeLocation.getFormatedDepartureTime()));
200                        } else {
201                            builder.append(String.format(locale, strings.getProperty("NoScheduledWorkAt"),  // NOI18N
202                                    routeLocationName));
203                        }
204                    } else {
205                        builder.append(String.format(locale, strings.getProperty("NoScheduledWorkAt"),  // NOI18N
206                                routeLocationName));
207                    }
208                } else {
209                    // if a route comment, then only use location name and route comment, useful for passenger
210                    // trains
211                    if (!routeLocation.getComment().isBlank()) {
212                        builder.append(String.format(locale, strings.getProperty("CommentAt"), // NOI18N
213                                routeLocationName, StringEscapeUtils
214                                        .escapeHtml4(routeLocation.getCommentWithColor())));
215                    }
216                    if (train.isShowArrivalAndDepartureTimesEnabled()) {
217                        if (routeLocation == train.getTrainDepartsRouteLocation()) {
218                            builder.append(String.format(locale, strings
219                                    .getProperty("CommentAtWithDepartureTime"), routeLocationName, train // NOI18N
220                                    .getFormatedDepartureTime(), StringEscapeUtils
221                                            .escapeHtml4(routeLocation.getCommentWithColor())));
222                        } else if (!routeLocation.getDepartureTime().equals(RouteLocation.NONE)) {
223                            builder.append(String.format(locale, strings
224                                    .getProperty("CommentAtWithDepartureTime"), routeLocationName, // NOI18N
225                                    routeLocation.getFormatedDepartureTime(), StringEscapeUtils
226                                            .escapeHtml4(routeLocation.getCommentWithColor())));
227                        } else if (Setup.isUseDepartureTimeEnabled() &&
228                                !routeLocation.getComment().isBlank()) {
229                            builder.append(String.format(locale, strings
230                                    .getProperty("NoScheduledWorkAtWithDepartureTime"), routeLocationName, // NOI18N
231                                    train.getExpectedDepartureTime(routeLocation)));
232                        }
233                    }                           
234                }
235                // add location comment
236                if (Setup.isPrintLocationCommentsEnabled() && !routeLocation.getLocation().getComment().isEmpty()) {
237                    builder.append(String.format(locale, strings.getProperty("LocationComment"), StringEscapeUtils  // NOI18N
238                            .escapeHtml4(routeLocation.getLocation().getCommentWithColor())));
239                }
240            }
241        } else {
242            builder.append(String.format(strings.getProperty("TrainTerminatesIn"), routeLocationName));  // NOI18N
243        }
244        return builder.toString();
245    }
246
247    private String performWork(boolean pickup, boolean local) {
248        if (pickup) {
249           return pickupCars();
250        } else {
251            return dropCars(local);
252        }
253    }
254
255    private String pickupCars() {
256        StringBuilder builder = new StringBuilder();
257        RouteLocation rlocation = train.getCurrentRouteLocation();
258        List<Car> carList = InstanceManager.getDefault(CarManager.class).getByTrainDestinationList(train);
259        List<Track> tracks = rlocation.getLocation().getTracksByNameList(null);
260        List<String> trackNames = new ArrayList<>();
261        List<String> pickedUp = new ArrayList<>();
262        this.clearUtilityCarTypes();
263        for (Track track : tracks) {
264            if (trackNames.contains(track.getSplitName())) {
265                continue;
266            }
267            trackNames.add(track.getSplitName()); // use a track name once
268            // block cars by destination
269            for (RouteLocation rld : train.getRoute().getLocationsBySequenceList()) {
270                for (Car car : carList) {
271                    if (pickedUp.contains(car.getId())
272                            || (Setup.isSortByTrackNameEnabled() && !track.getSplitName().equals(
273                                    car.getSplitTrackName()))) {
274                        continue;
275                    }
276                    if (car.isLocalMove() && rlocation == rld) {
277                        continue;
278                    }
279                    // block pick up cars
280                    // caboose or FRED is placed at end of the train
281                    // passenger cars are already blocked in the car list
282                    // passenger cars with negative block numbers are placed at
283                    // the front of the train, positive numbers at the end of
284                    // the train.
285                    // note that a car in train doesn't have a track assignment
286                    if (isNextCar(car, rlocation, rld)) {
287                        pickedUp.add(car.getId());
288                        if (car.isUtility()) {
289                            builder.append(pickupUtilityCars(carList, car, TrainCommon.IS_MANIFEST));
290                         // use truncated format if there's a switch list
291                        } else if (Setup.isPrintTruncateManifestEnabled() && rlocation.getLocation().isSwitchListEnabled()) {
292                            builder.append(pickUpCar(car, Setup.getPickupTruncatedManifestMessageFormat()));
293                        } else {
294                            builder.append(pickUpCar(car, Setup.getPickupManifestMessageFormat()));
295                        }
296                    }
297                }
298            }
299        }
300        return builder.toString();
301    }
302
303    private String dropCars(boolean local) {
304        StringBuilder builder = new StringBuilder();
305        RouteLocation location = train.getCurrentRouteLocation();
306        List<Car> carList = InstanceManager.getDefault(CarManager.class).getByTrainDestinationList(train);
307        List<Track> tracks = location.getLocation().getTracksByNameList(null);
308        List<String> trackNames = new ArrayList<>();
309        List<String> dropped = new ArrayList<>();
310        for (Track track : tracks) {
311            if (trackNames.contains(track.getSplitName())) {
312                continue;
313            }
314            trackNames.add(track.getSplitName()); // use a track name once
315            for (Car car : carList) {
316                if (dropped.contains(car.getId())
317                        || (Setup.isSortByTrackNameEnabled() && !track.getSplitName().equals(
318                                car.getSplitDestinationTrackName()))) {
319                    continue;
320                }
321                if (car.isLocalMove() == local
322                        && (car.getRouteDestination() == location && car.getDestinationTrack() != null)) {
323                    dropped.add(car.getId());
324                    if (car.isUtility()) {
325                        builder.append(setoutUtilityCars(carList, car, local));
326                    } else {
327                        String[] format = (!local) ? Setup.getDropManifestMessageFormat() : Setup
328                                .getLocalManifestMessageFormat();
329                        builder.append(dropCar(car, format, local));
330                    }
331                }
332            }
333        }
334        return builder.toString();
335    }
336}