001package jmri.web.servlet.operations;
002
003import java.io.IOException;
004import java.text.ParseException;
005import java.util.*;
006import java.util.Map.Entry;
007
008import org.apache.commons.text.StringEscapeUtils;
009import org.slf4j.Logger;
010import org.slf4j.LoggerFactory;
011
012import com.fasterxml.jackson.databind.JsonNode;
013import com.fasterxml.jackson.databind.ObjectMapper;
014import com.fasterxml.jackson.databind.util.StdDateFormat;
015
016import jmri.InstanceManager;
017import jmri.jmrit.operations.rollingstock.Xml;
018import jmri.jmrit.operations.rollingstock.cars.Car;
019import jmri.jmrit.operations.rollingstock.cars.CarManager;
020import jmri.jmrit.operations.routes.RouteLocation;
021import jmri.jmrit.operations.setup.Setup;
022import jmri.jmrit.operations.trains.JsonManifest;
023import jmri.jmrit.operations.trains.Train;
024import jmri.jmrit.operations.trains.schedules.TrainScheduleManager;
025import jmri.server.json.JSON;
026import jmri.server.json.operations.JsonOperations;
027
028/**
029 *
030 * @author Randall Wood
031 */
032public class HtmlManifest extends HtmlTrainCommon {
033
034    protected ObjectMapper mapper;
035    private JsonNode jsonManifest = null;
036    private final static Logger log = LoggerFactory.getLogger(HtmlManifest.class);
037
038    public HtmlManifest(Locale locale, Train train) throws IOException {
039        super(locale, train);
040        this.mapper = new ObjectMapper();
041        this.resourcePrefix = "Manifest";
042    }
043
044    // TODO cache the results so a quick check that if the JsonManifest file is not
045    // newer than the Html manifest, the cached copy is returned instead.
046    public String getLocations() throws IOException {
047        // build manifest from JSON manifest
048        if (this.getJsonManifest() == null) {
049            return "Error manifest file not found for this train";
050        }
051        StringBuilder builder = new StringBuilder();
052        JsonNode locations = this.getJsonManifest().path(JsonOperations.LOCATIONS);
053        String previousLocationName = null;
054        boolean hasWork;
055        for (JsonNode location : locations) {
056            RouteLocation routeLocation = train.getRoute().getLocationById(location.path(JSON.NAME).textValue());
057            log.debug("Processing {} ({})", routeLocation.getName(), location.path(JSON.NAME).textValue());
058            String routeLocationName = location.path(JSON.USERNAME).textValue();
059            builder.append(String.format(locale, strings.getProperty("LocationStart"), routeLocation.getId())); // NOI18N
060            hasWork = (location.path(JsonOperations.CARS).path(JSON.ADD).size() > 0
061                    || location.path(JsonOperations.CARS).path(JSON.REMOVE).size() > 0
062                    || location.path(JSON.ENGINES).path(JSON.ADD).size() > 0 || location.path(JSON.ENGINES).path(
063                            JSON.REMOVE).size() > 0);
064            if (hasWork && !routeLocationName.equals(previousLocationName)) {
065                if (!train.isShowArrivalAndDepartureTimesEnabled()) {
066                    builder.append(String.format(locale, strings.getProperty("ScheduledWorkAt"), routeLocationName)); // NOI18N
067                } else if (routeLocation == train.getTrainDepartsRouteLocation()) {
068                    builder.append(String.format(locale, strings.getProperty("WorkDepartureTime"), routeLocationName,
069                            train.getFormatedDepartureTime())); // NOI18N
070                } else if (!routeLocation.getDepartureTime().equals(RouteLocation.NONE)) {
071                    builder.append(String.format(locale, strings.getProperty("WorkDepartureTime"), routeLocationName,
072                            routeLocation.getFormatedDepartureTime())); // NOI18N
073                } else if (Setup.isUseDepartureTimeEnabled()
074                        && routeLocation != train.getTrainTerminatesRouteLocation()) {
075                    builder.append(String.format(locale, strings.getProperty("WorkDepartureTime"), routeLocationName,
076                            train.getExpectedDepartureTime(routeLocation))); // NOI18N
077                } else if (!train.getExpectedArrivalTime(routeLocation).equals(Train.ALREADY_SERVICED)) { // NOI18N
078                    builder.append(String.format(locale, strings.getProperty("WorkArrivalTime"), routeLocationName,
079                            train.getExpectedArrivalTime(routeLocation))); // NOI18N
080                } else {
081                    builder.append(String.format(locale, strings.getProperty("ScheduledWorkAt"), routeLocationName)); // NOI18N
082                }
083                // add route comment
084                if (!location.path(JSON.COMMENT).textValue().isBlank()) {
085                    builder.append(String.format(locale, strings.getProperty("RouteLocationComment"), 
086                            location.path(JSON.COMMENT).textValue()));
087                }
088
089                // add location comment
090                if (Setup.isPrintLocationCommentsEnabled()
091                        && !location.path(JsonOperations.LOCATION).path(JSON.COMMENT).textValue().isBlank()) {
092                    builder.append(String.format(locale, strings.getProperty("LocationComment"), location.path(
093                            JsonOperations.LOCATION).path(JSON.COMMENT).textValue()));
094                }
095
096                // add track comments
097                builder.append(
098                        getTrackComments(location.path(JsonOperations.TRACK), location.path(JsonOperations.CARS)));
099            }
100
101            previousLocationName = routeLocationName;
102
103            // engine change or helper service?
104            if (location.path(JSON.OPTIONS).size() > 0) {
105                boolean changeEngines = false;
106                boolean changeCaboose = false;
107                for (JsonNode option : location.path(JSON.OPTIONS)) {
108                    switch (option.asText()) {
109                        case JSON.CHANGE_ENGINES:
110                            changeEngines = true;
111                            break;
112                        case JSON.CHANGE_CABOOSE:
113                            changeCaboose = true;
114                            break;
115                        case JSON.ADD_HELPERS:
116                            builder.append(String.format(strings.getProperty("AddHelpersAt"), routeLocationName));
117                            break;
118                        case JSON.REMOVE_HELPERS:
119                            builder.append(String.format(strings.getProperty("RemoveHelpersAt"), routeLocationName));
120                            break;
121                        default:
122                            break;
123                    }
124                }
125                if (changeEngines && changeCaboose) {
126                    builder.append(String.format(strings.getProperty("LocoAndCabooseChangeAt"), routeLocationName)); // NOI18N
127                } else if (changeEngines) {
128                    builder.append(String.format(strings.getProperty("LocoChangeAt"), routeLocationName)); // NOI18N
129                } else if (changeCaboose) {
130                    builder.append(String.format(strings.getProperty("CabooseChangeAt"), routeLocationName)); // NOI18N
131                }
132            }
133
134            builder.append(pickupEngines(location.path(JSON.ENGINES).path(JSON.ADD)));
135            builder.append(blockCars(location.path(JsonOperations.CARS), routeLocation, true));
136            builder.append(dropEngines(location.path(JSON.ENGINES).path(JSON.REMOVE)));
137
138            if (routeLocation != train.getTrainTerminatesRouteLocation()) {
139                // Is the next location the same as the current?
140                RouteLocation rlNext = train.getRoute().getNextRouteLocation(routeLocation);
141                if (!routeLocationName.equals(rlNext.getSplitName())) {
142                    if (hasWork) {
143                        if (!Setup.isPrintLoadsAndEmptiesEnabled()) {
144                            // Message format: Train departs Boston Westbound with 12 cars, 450 feet, 3000 tons
145                            builder.append(String.format(strings.getProperty("TrainDepartsCars"), routeLocationName,
146                                    strings.getProperty("Heading"
147                                            + Setup.getDirectionString(location.path(JSON.TRAIN_DIRECTION).intValue())),
148                                    location.path(JSON.LENGTH).path(JSON.LENGTH).intValue(), location.path(JSON.LENGTH)
149                                    .path(JSON.UNIT).asText().toLowerCase(), location.path(JsonOperations.WEIGHT)
150                                    .intValue(), location.path(JsonOperations.CARS).path(JSON.TOTAL).intValue()));
151                        } else {
152                            // Message format: Train departs Boston Westbound with 4 loads, 8 empties, 450 feet, 3000
153                            // tons
154                            builder.append(String.format(strings.getProperty("TrainDepartsLoads"), routeLocationName,
155                                    strings.getProperty("Heading"
156                                            + Setup.getDirectionString(location.path(JSON.TRAIN_DIRECTION).intValue())),
157                                    location.path(JSON.LENGTH).path(JSON.LENGTH).intValue(), location.path(JSON.LENGTH)
158                                    .path(JSON.UNIT).asText().toLowerCase(), location.path(JsonOperations.WEIGHT)
159                                    .intValue(), location.path(JsonOperations.CARS).path(JSON.LOADS).intValue(), location
160                                    .path(JsonOperations.CARS).path(JSON.EMPTIES).intValue()));
161                        }
162                    } else {
163                        log.debug("No work ({})", routeLocation.getComment());
164                        if (routeLocation.getComment().isBlank()) {
165                            // no route comment, no work at this location
166                            if (train.isShowArrivalAndDepartureTimesEnabled()) {
167                                if (routeLocation == train.getTrainDepartsRouteLocation()) {
168                                    builder.append(String.format(locale, strings
169                                            .getProperty("NoScheduledWorkAtWithDepartureTime"), routeLocationName,
170                                            train.getFormatedDepartureTime()));
171                                } else if (!routeLocation.getDepartureTime().isEmpty()) {
172                                    builder.append(String.format(locale, strings
173                                            .getProperty("NoScheduledWorkAtWithDepartureTime"), routeLocationName,
174                                            routeLocation.getFormatedDepartureTime()));
175                                } else if (Setup.isUseDepartureTimeEnabled()) {
176                                    builder.append(String.format(locale, strings
177                                            .getProperty("NoScheduledWorkAtWithDepartureTime"), routeLocationName,
178                                            location.path(JSON.EXPECTED_DEPARTURE)));
179                                } else { // fall back to generic no scheduled work message
180                                    builder.append(String.format(locale, strings.getProperty("NoScheduledWorkAt"),
181                                            routeLocationName));
182                                }
183                            } else {
184                                builder.append(String.format(locale, strings.getProperty("NoScheduledWorkAt"),
185                                        routeLocationName));
186                            }
187                        } else {
188                            // if a route comment, then only use location name and route comment, useful for passenger
189                            // trains
190                            if (!routeLocation.getComment().isBlank()) {
191                                builder.append(String.format(locale, strings.getProperty("CommentAt"), // NOI18N
192                                        routeLocationName, StringEscapeUtils
193                                                .escapeHtml4(routeLocation.getCommentWithColor())));
194                            }
195                            if (train.isShowArrivalAndDepartureTimesEnabled()) {
196                                if (routeLocation == train.getTrainDepartsRouteLocation()) {
197                                    builder.append(String.format(locale, strings
198                                            .getProperty("CommentAtWithDepartureTime"), routeLocationName, train // NOI18N
199                                            .getFormatedDepartureTime(), StringEscapeUtils
200                                            .escapeHtml4(routeLocation.getComment())));
201                                } else if (!routeLocation.getDepartureTime().equals(RouteLocation.NONE)) {
202                                    builder.append(String.format(locale, strings
203                                            .getProperty("CommentAtWithDepartureTime"), routeLocationName, // NOI18N
204                                            routeLocation.getFormatedDepartureTime(), StringEscapeUtils
205                                            .escapeHtml4(routeLocation.getComment())));
206                                } else if (Setup.isUseDepartureTimeEnabled() &&
207                                        !routeLocation.getComment().equals(RouteLocation.NONE)) {
208                                    builder.append(String.format(locale, strings
209                                            .getProperty("NoScheduledWorkAtWithDepartureTime"), routeLocationName, // NOI18N
210                                            train.getExpectedDepartureTime(routeLocation)));
211                                }
212                            }                           
213                        }
214                        // add location comment
215                        if (Setup.isPrintLocationCommentsEnabled()
216                                && !routeLocation.getLocation().getComment().isEmpty()) {
217                            builder.append(String.format(locale, strings.getProperty("LocationComment"),
218                                    StringEscapeUtils.escapeHtml4(routeLocation.getLocation().getCommentWithColor())));
219                        }
220                    }
221                }
222            } else {
223                builder.append(String.format(strings.getProperty("TrainTerminatesIn"), routeLocationName));
224            }
225        }
226        return builder.toString();
227    }
228
229    protected String blockCars(JsonNode cars, RouteLocation location, boolean isManifest) {
230        StringBuilder builder = new StringBuilder();
231        log.debug("Cars is {}", cars);
232
233        //copy the adds into a sortable arraylist
234        ArrayList<JsonNode> adds = new ArrayList<JsonNode>();
235        cars.path(JSON.ADD).forEach(adds::add);
236            
237        //sort if requested
238        if (adds.size() > 0 && Setup.isSortByTrackNameEnabled()) {
239            adds.sort(Comparator.comparing(o -> o.path("location").path("track").path("userName").asText()));
240        }
241        //format each car for output
242        // use truncated format if there's a switch list
243        for (JsonNode car : adds) {
244            if (!this.isLocalMove(car)) {
245                if (this.isUtilityCar(car)) {
246                    builder.append(pickupUtilityCars(adds, car, location, isManifest));
247                } else if (isManifest &&
248                        Setup.isPrintTruncateManifestEnabled() &&
249                        location.getLocation().isSwitchListEnabled()) {
250                    builder.append(pickUpCar(car, Setup.getPickupTruncatedManifestMessageFormat()));
251                } else {
252                    builder.append(pickUpCar(car, Setup.getPickupManifestMessageFormat()));
253                }
254            }
255        }
256
257        //copy the drops into a sortable arraylist
258        ArrayList<JsonNode> drops = new ArrayList<JsonNode>();
259        cars.path(JSON.REMOVE).forEach(drops::add);
260
261        for (JsonNode car : drops) {
262            boolean local = isLocalMove(car);
263            if (this.isUtilityCar(car)) {
264                builder.append(setoutUtilityCars(drops, car, location, isManifest));
265            } else if (isManifest &&
266                    Setup.isPrintTruncateManifestEnabled() &&
267                    location.getLocation().isSwitchListEnabled() &&
268                    !train.isLocalSwitcher()) {
269                builder.append(dropCar(car, Setup.getDropTruncatedManifestMessageFormat(), local));
270            } else {
271                String[] format;
272                if (isManifest) {
273                    format = (!local) ? Setup.getDropManifestMessageFormat() : Setup
274                            .getLocalManifestMessageFormat();
275                } else {
276                    format = (!local) ? Setup.getDropSwitchListMessageFormat() : Setup
277                            .getLocalSwitchListMessageFormat();
278                }
279                builder.append(dropCar(car, format, local));
280            }
281        }
282        return String.format(locale, strings.getProperty("CarsList"), builder.toString());
283    }
284
285    protected String pickupUtilityCars(ArrayList<JsonNode> jnCars, JsonNode jnCar, RouteLocation location,
286            boolean isManifest) {
287        List<Car> cars = getCarList(jnCars);
288        Car car = getCar(jnCar);
289        return pickupUtilityCars(cars, car, isManifest);
290    }
291
292    protected String setoutUtilityCars(ArrayList<JsonNode> jnCars, JsonNode jnCar, RouteLocation location,
293            boolean isManifest) {
294        List<Car> cars = getCarList(jnCars);
295        Car car = getCar(jnCar);
296        return setoutUtilityCars(cars, car, isManifest);
297    }
298
299    protected List<Car> getCarList(ArrayList<JsonNode> jnCars) {
300        List<Car> cars = new ArrayList<>();
301        for (JsonNode kar : jnCars) { 
302            cars.add(getCar(kar));
303        }
304        return cars;
305    }
306    
307    protected Car getCar(JsonNode jnCar) {
308        String id = jnCar.path(JSON.NAME).asText();
309        Car car = InstanceManager.getDefault(CarManager.class).getById(id);
310        return car;
311    }
312
313    protected String pickUpCar(JsonNode car, String[] format) {
314        if (isLocalMove(car)) {
315            return ""; // print nothing for local move, see dropCar()
316        }
317        StringBuilder builder = new StringBuilder();
318        builder.append(Setup.getPickupCarPrefix()).append(" ");
319        for (String attribute : format) {
320            if (!attribute.trim().isEmpty()) {
321                attribute = attribute.toLowerCase();
322                log.trace("Adding car with attribute {}", attribute);
323                if (attribute.equals(JsonOperations.LOCATION) || attribute.equals(JsonOperations.TRACK)) {
324                    attribute = JsonOperations.LOCATION; // treat "track" as "location"
325                    builder.append(
326                            this.getFormattedAttribute(attribute, this.getPickupLocation(car.path(attribute),
327                                            ShowLocation.track))).append(" "); // NOI18N
328                } else if (attribute.equals(JsonOperations.DESTINATION)) {
329                    builder.append(
330                            this.getFormattedAttribute(attribute, this.getDropLocation(car.path(attribute),
331                                            ShowLocation.location))).append(" "); // NOI18N
332                } else if (attribute.equals(JsonOperations.DESTINATION_TRACK)) {
333                    builder.append(
334                            this.getFormattedAttribute(attribute, this.getDropLocation(car.path(JsonOperations.DESTINATION),
335                                            ShowLocation.both))).append(" "); // NOI18N
336                } else if (attribute.equals(Xml.TYPE)) {
337                    builder.append(this.getTextAttribute(JsonOperations.CAR_TYPE, car)).append(" "); // NOI18N
338                } else {
339                    builder.append(this.getTextAttribute(attribute, car)).append(" "); // NOI18N
340                }
341            }
342        }
343        log.debug("Picking up car {}", builder);
344        return String.format(locale, strings.getProperty(this.resourcePrefix + "PickUpCar"), builder.toString()); // NOI18N
345    }
346
347    protected String dropCar(JsonNode car, String[] format, boolean isLocal) {
348        StringBuilder builder = new StringBuilder();
349        if (!isLocal) {
350            builder.append(Setup.getDropCarPrefix()).append(" ");
351        } else {
352            builder.append(Setup.getLocalPrefix()).append(" ");
353        }
354        log.debug("dropCar {}", car);
355        for (String attribute : format) {
356            if (!attribute.trim().isEmpty()) {
357                attribute = attribute.toLowerCase();
358                log.trace("Removing car with attribute {}", attribute);
359                if (attribute.equals(JsonOperations.DESTINATION) || attribute.equals(JsonOperations.TRACK)) {
360                    attribute = JsonOperations.DESTINATION; // treat "track" as "destination"
361                    builder.append(
362                            this.getFormattedAttribute(attribute, this.getDropLocation(car.path(attribute),
363                                            ShowLocation.track))).append(" "); // NOI18N
364                } else if (attribute.equals(JsonOperations.LOCATION) && isLocal) {
365                    builder.append(
366                            this.getFormattedAttribute(attribute, this.getPickupLocation(car.path(attribute),
367                                            ShowLocation.track))).append(" "); // NOI18N
368                } else if (attribute.equals(JsonOperations.LOCATION)) {
369                    builder.append(
370                            this.getFormattedAttribute(attribute, this.getPickupLocation(car.path(attribute),
371                                            ShowLocation.location))).append(" "); // NOI18N
372                } else if (attribute.equals(Xml.TYPE)) {
373                    builder.append(this.getTextAttribute(JsonOperations.CAR_TYPE, car)).append(" "); // NOI18N
374                } else {
375                    builder.append(this.getTextAttribute(attribute, car)).append(" "); // NOI18N
376                }
377            }
378        }
379        log.debug("Dropping {}car {}", (isLocal) ? "local " : "", builder);
380        if (!isLocal) {
381            return String.format(locale, strings.getProperty(this.resourcePrefix + "DropCar"), builder.toString()); // NOI18N
382        } else {
383            return String.format(locale, strings.getProperty(this.resourcePrefix + "LocalCar"), builder.toString()); // NOI18N
384        }
385    }
386
387    protected String dropEngines(JsonNode engines) {
388        StringBuilder builder = new StringBuilder();
389        engines.forEach((engine) -> {
390            builder.append(this.dropEngine(engine));
391        });
392        return String.format(locale, strings.getProperty("EnginesList"), builder.toString());
393    }
394
395    protected String dropEngine(JsonNode engine) {
396        StringBuilder builder = new StringBuilder();
397        builder.append(Setup.getDropEnginePrefix()).append(" ");
398        for (String attribute : Setup.getDropEngineMessageFormat()) {
399            if (!attribute.trim().isEmpty()) {
400                attribute = attribute.toLowerCase();
401                if (attribute.equals(JsonOperations.DESTINATION) || attribute.equals(JsonOperations.TRACK)) {
402                    attribute = JsonOperations.DESTINATION; // treat "track" as "destination"
403                    builder.append(
404                            this.getFormattedAttribute(attribute, this.getDropLocation(engine.path(attribute),
405                                            ShowLocation.track))).append(" "); // NOI18N
406                } else {
407                    builder.append(this.getTextAttribute(attribute, engine)).append(" "); // NOI18N
408                }
409            }
410        }
411        log.debug("Drop engine: {}", builder);
412        return String.format(locale, strings.getProperty(this.resourcePrefix + "DropEngine"), builder.toString());
413    }
414
415    protected String pickupEngines(JsonNode engines) {
416        StringBuilder builder = new StringBuilder();
417        if (engines.size() > 0) {
418            for (JsonNode engine : engines) {
419                builder.append(this.pickupEngine(engine));
420            }
421        }
422        return String.format(locale, strings.getProperty("EnginesList"), builder.toString());
423    }
424
425    protected String pickupEngine(JsonNode engine) {
426        StringBuilder builder = new StringBuilder();
427        builder.append(Setup.getPickupEnginePrefix()).append(" ");
428        log.debug("PickupEngineMessageFormat: {}", (Object) Setup.getPickupEngineMessageFormat());
429        for (String attribute : Setup.getPickupEngineMessageFormat()) {
430            if (!attribute.trim().isEmpty()) {
431                attribute = attribute.toLowerCase();
432                if (attribute.equals(JsonOperations.LOCATION) || attribute.equals(JsonOperations.TRACK)) {
433                    attribute = JsonOperations.LOCATION; // treat "track" as "location"
434                    builder.append(
435                            this.getFormattedAttribute(attribute, this.getPickupLocation(engine.path(attribute),
436                                            ShowLocation.track))).append(" "); // NOI18N
437                } else {
438                    builder.append(this.getTextAttribute(attribute, engine)).append(" "); // NOI18N
439                }
440            }
441        }
442        log.debug("Picking up engine: {}", builder);
443        return String.format(locale, strings.getProperty(this.resourcePrefix + "PickUpEngine"), builder.toString());
444    }
445
446    protected String getDropLocation(JsonNode location, ShowLocation show) {
447        return this.getFormattedLocation(location, show, "To"); // NOI18N
448    }
449
450    protected String getPickupLocation(JsonNode location, ShowLocation show) {
451        return this.getFormattedLocation(location, show, "From"); // NOI18N
452    }
453
454    protected String getTextAttribute(String attribute, JsonNode rollingStock) {
455        if (attribute.equals(JSON.HAZARDOUS)) {
456            return this.getFormattedAttribute(attribute, (rollingStock.path(attribute).asBoolean() ? Setup
457                    .getHazardousMsg() : "")); // NOI18N
458        } else if (attribute.equals(Setup.PICKUP_COMMENT.toLowerCase())) { // NOI18N
459            return this.getFormattedAttribute(JSON.ADD_COMMENT, rollingStock.path(JSON.ADD_COMMENT).textValue());
460        } else if (attribute.equals(Setup.DROP_COMMENT.toLowerCase())) { // NOI18N
461            return this.getFormattedAttribute(JSON.REMOVE_COMMENT, rollingStock.path(JSON.REMOVE_COMMENT).textValue());
462        } else if (attribute.equals(Setup.RWE.toLowerCase())) {
463            return this.getFormattedLocation(rollingStock.path(JSON.RETURN_WHEN_EMPTY), ShowLocation.both, "RWE"); // NOI18N
464        } else if (attribute.equals(Setup.FINAL_DEST.toLowerCase())) {
465            return this.getFormattedLocation(rollingStock.path(JSON.FINAL_DESTINATION), ShowLocation.location, "FinalDestination"); // NOI18N
466        } else if (attribute.equals(Setup.FINAL_DEST_TRACK.toLowerCase())) {
467            return this.getFormattedLocation(rollingStock.path(JSON.FINAL_DESTINATION), ShowLocation.track, "FinalDestination"); // NOI18N
468        }
469        return this.getFormattedAttribute(attribute, rollingStock.path(attribute).asText());
470    }
471
472    protected String getFormattedAttribute(String attribute, String value) {
473        return String.format(locale, strings.getProperty("Attribute"), StringEscapeUtils.escapeHtml4(value), attribute);
474    }
475
476    protected String getFormattedLocation(JsonNode location, ShowLocation show, String prefix) {
477        if (location.isNull() || location.isEmpty()) {
478            // return an empty string if location is an empty or null
479            return "";
480        }
481        // TODO handle tracks without names
482        switch (show) {
483            case location:
484                return String.format(locale, strings.getProperty(prefix + "Location"),
485                        splitString(location.path(JSON.USERNAME).asText()));
486            case track:
487                return String.format(locale, strings.getProperty(prefix + "Track"),
488                        splitString(location.path(JsonOperations.TRACK).path(JSON.USERNAME).asText()));
489            case both:
490            default: // default here ensures the method always returns
491                return String.format(locale, strings.getProperty(prefix + "LocationAndTrack"),
492                        splitString(location.path(JSON.USERNAME).asText()),
493                        splitString(location.path(JsonOperations.TRACK).path(JSON.USERNAME).asText()));
494        }
495    }
496
497    private String getTrackComments(JsonNode tracks, JsonNode cars) {
498        StringBuilder builder = new StringBuilder();
499        if (tracks.size() > 0) {
500            Iterator<Entry<String, JsonNode>> iterator = tracks.fields();
501            while (iterator.hasNext()) {
502                Entry<String, JsonNode> track = iterator.next();
503                boolean pickup = false;
504                boolean setout = false;
505                if (cars.path(JSON.ADD).size() > 0) {
506                    for (JsonNode car : cars.path(JSON.ADD)) {
507                        if (track.getKey().equals(car.path(JsonOperations.LOCATION).path(JsonOperations.TRACK)
508                                .path(JSON.NAME).asText())) {
509                            pickup = true;
510                            break; // we do not need to iterate all cars
511                        }
512                    }
513                }
514                if (cars.path(JSON.REMOVE).size() > 0) {
515                    for (JsonNode car : cars.path(JSON.REMOVE)) {
516                        if (track.getKey().equals(car.path(JsonOperations.DESTINATION).path(JsonOperations.TRACK)
517                                .path(JSON.NAME).textValue())) {
518                            setout = true;
519                            break; // we do not need to iterate all cars
520                        }
521                    }
522                }
523                if (pickup && setout) {
524                    builder.append(String.format(locale, strings.getProperty("TrackComments"), track.getValue().path(
525                            JSON.ADD_AND_REMOVE).textValue()));
526                } else if (pickup) {
527                    builder.append(String.format(locale, strings.getProperty("TrackComments"), track.getValue().path(
528                            JSON.ADD).textValue()));
529                } else if (setout) {
530                    builder.append(String.format(locale, strings.getProperty("TrackComments"), track.getValue().path(
531                            JSON.REMOVE).textValue()));
532                }
533            }
534        }
535        return builder.toString();
536    }
537
538    protected boolean isLocalMove(JsonNode car) {
539        return car.path(JSON.IS_LOCAL).booleanValue();        
540    }
541
542    protected boolean isUtilityCar(JsonNode car) {
543        return car.path(JSON.UTILITY).booleanValue();
544    }
545
546    protected JsonNode getJsonManifest() throws IOException {
547        if (this.jsonManifest == null) {
548            try {
549                this.jsonManifest = this.mapper.readTree((new JsonManifest(this.train)).getFile());
550            } catch (IOException e) {
551                log.error("Json manifest file not found for train ({})", this.train.getName());
552            }
553        }
554        return this.jsonManifest;
555    }
556
557    @Override
558    public String getValidity() {
559        try {
560            if (Setup.isPrintTrainScheduleNameEnabled()) {
561                return String.format(locale, strings.getProperty(this.resourcePrefix + "ValidityWithSchedule"),
562                        getDate((new StdDateFormat()).parse(this.getJsonManifest().path(JsonOperations.DATE).textValue())),
563                        InstanceManager.getDefault(TrainScheduleManager.class).getActiveSchedule().getName());
564            } else {
565                return String.format(locale, strings.getProperty(this.resourcePrefix + "Validity"),
566                        getDate((new StdDateFormat()).parse(this.getJsonManifest().path(JsonOperations.DATE).textValue())));
567            }
568        } catch (NullPointerException ex) {
569            log.warn("Manifest for train {} (id {}) does not have any validity.", this.train.getIconName(), this.train
570                    .getId());
571        } catch (ParseException ex) {
572            log.error("Date of JSON manifest could not be parsed as a Date.");
573        } catch (IOException ex) {
574            log.error("JSON manifest could not be read.");
575        }
576        return "";
577    }
578}