001package jmri.jmrit.operations.trains;
002
003import java.io.File;
004import java.io.IOException;
005import java.util.List;
006import java.util.Locale;
007
008import org.apache.commons.text.StringEscapeUtils;
009import org.slf4j.Logger;
010import org.slf4j.LoggerFactory;
011
012import com.fasterxml.jackson.databind.ObjectMapper;
013import com.fasterxml.jackson.databind.SerializationFeature;
014import com.fasterxml.jackson.databind.node.ArrayNode;
015import com.fasterxml.jackson.databind.node.ObjectNode;
016
017import jmri.InstanceManager;
018import jmri.jmrit.operations.locations.Track;
019import jmri.jmrit.operations.rollingstock.cars.Car;
020import jmri.jmrit.operations.rollingstock.engines.Engine;
021import jmri.jmrit.operations.routes.RouteLocation;
022import jmri.jmrit.operations.setup.Setup;
023import jmri.jmrit.operations.trains.trainbuilder.TrainCommon;
024import jmri.server.json.JSON;
025import jmri.server.json.operations.JsonOperations;
026import jmri.server.json.operations.JsonUtil;
027
028/**
029 * A minimal manifest in JSON.
030 *
031 * This manifest is intended to be read by machines for building manifests in
032 * other, human-readable outputs. This manifest is retained at build time so
033 * that manifests can be endlessly recreated in other formats, even if the
034 * operations database state has changed. It is expected that the parsers for
035 * this manifest will be capable of querying operations for more specific
036 * information while transforming this manifest into other formats.
037 *
038 * @author Randall Wood
039 * @author Daniel Boudreau 1/26/2015 Load all cars including utility cars into
040 * the JSON file, and tidied up the code a bit.
041 *
042 */
043public class JsonManifest extends TrainCommon {
044
045    protected final Locale locale = Locale.getDefault();
046    protected final Train train;
047    private final ObjectMapper mapper = new ObjectMapper();
048    private final JsonUtil utilities = new JsonUtil(mapper);
049
050    private final static Logger log = LoggerFactory.getLogger(JsonManifest.class);
051
052    public JsonManifest(Train train) {
053        this.train = train;
054        this.mapper.enable(SerializationFeature.INDENT_OUTPUT);
055    }
056
057    public File getFile() {
058        return InstanceManager.getDefault(TrainManagerXml.class).getManifestFile(this.train.getName(), JSON.JSON);
059    }
060
061    public void build() throws IOException {
062        ObjectNode root = this.mapper.createObjectNode();
063        if (!this.train.getRailroadName().equals(Train.NONE)) {
064            root.put(JSON.RAILROAD, StringEscapeUtils.escapeHtml4(this.train.getRailroadName()));
065        } else {
066            root.put(JSON.RAILROAD, StringEscapeUtils.escapeHtml4(Setup.getRailroadName()));
067        }
068        root.put(JSON.USERNAME, StringEscapeUtils.escapeHtml4(this.train.getName()));
069        root.put(JSON.DESCRIPTION, StringEscapeUtils.escapeHtml4(this.train.getDescription()));
070        root.set(JSON.LOCATIONS, this.getLocations());
071        if (!this.train.getManifestLogoPathName().equals(Train.NONE)) {
072            // The operationsServlet will need to change this to a usable URL
073            root.put(JSON.IMAGE, this.train.getManifestLogoPathName());
074        }
075        root.put(JsonOperations.DATE, TrainCommon.getISO8601Date(true)); // Validity
076        this.mapper.writeValue(InstanceManager.getDefault(TrainManagerXml.class).createManifestFile(this.train.getName(), JSON.JSON), root);
077    }
078
079    public ArrayNode getLocations() {
080        // get engine and car lists
081        List<Engine> engineList = engineManager.getByTrainBlockingList(train);
082        List<Car> carList = carManager.getByTrainDestinationList(train);
083        ArrayNode locations = this.mapper.createArrayNode();
084        List<RouteLocation> route = train.getRoute().getLocationsBySequenceList();
085        for (RouteLocation routeLocation : route) {
086            String locationName = routeLocation.getSplitName();
087            ObjectNode jsonLocation = this.mapper.createObjectNode();
088            ObjectNode jsonCars = this.mapper.createObjectNode();
089            jsonLocation.put(JSON.USERNAME, StringEscapeUtils.escapeHtml4(locationName));
090            jsonLocation.put(JSON.NAME, routeLocation.getId());
091            if (routeLocation != train.getTrainDepartsRouteLocation()) {
092                jsonLocation.put(JSON.ARRIVAL_TIME, train.getExpectedArrivalTime(routeLocation));
093            }
094            if (routeLocation == train.getTrainDepartsRouteLocation()) {
095                jsonLocation.put(JSON.DEPARTURE_TIME, train.getDepartureTime());
096            } else if (!routeLocation.getDepartureTime().equals(RouteLocation.NONE)) {
097                jsonLocation.put(JSON.DEPARTURE_TIME, routeLocation.getDepartureTime());
098            } else {
099                jsonLocation.put(JSON.EXPECTED_DEPARTURE, train.getExpectedDepartureTime(routeLocation));
100            }
101            // add location comment and id
102            ObjectNode locationNode = this.mapper.createObjectNode();
103            locationNode.put(JSON.COMMENT,
104                    StringEscapeUtils.escapeHtml4(routeLocation.getLocation().getCommentWithColor()));
105            locationNode.put(JSON.NAME, routeLocation.getLocation().getId());
106            jsonLocation.set(JSON.LOCATION, locationNode);
107            jsonLocation.put(JSON.COMMENT, StringEscapeUtils.escapeHtml4(routeLocation.getCommentWithColor()));
108            // engine change or helper service?
109            if (train.getSecondLegOptions() != Train.NO_CABOOSE_OR_FRED) {
110                ArrayNode options = this.mapper.createArrayNode();
111                if (routeLocation == train.getSecondLegStartRouteLocation()) {
112                    if ((train.getSecondLegOptions() & Train.HELPER_ENGINES) == Train.HELPER_ENGINES) {
113                        options.add(JSON.ADD_HELPERS);
114                    } else if ((train.getSecondLegOptions() & Train.REMOVE_CABOOSE) == Train.REMOVE_CABOOSE
115                            || (train.getSecondLegOptions() & Train.ADD_CABOOSE) == Train.ADD_CABOOSE) {
116                        options.add(JSON.CHANGE_CABOOSE);
117                    } else if ((train.getSecondLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES) {
118                        options.add(JSON.CHANGE_ENGINES);
119                    }
120                }
121                if (routeLocation == train.getSecondLegEndRouteLocation()) {
122                    options.add(JSON.REMOVE_HELPERS);
123                }
124                jsonLocation.set(JSON.OPTIONS, options);
125            }
126            if (train.getThirdLegOptions() != Train.NO_CABOOSE_OR_FRED) {
127                ArrayNode options = this.mapper.createArrayNode();
128                if (routeLocation == train.getThirdLegStartRouteLocation()) {
129                    if ((train.getThirdLegOptions() & Train.HELPER_ENGINES) == Train.HELPER_ENGINES) {
130                        options.add(JSON.ADD_HELPERS);
131                    } else if ((train.getThirdLegOptions() & Train.REMOVE_CABOOSE) == Train.REMOVE_CABOOSE
132                            || (train.getThirdLegOptions() & Train.ADD_CABOOSE) == Train.ADD_CABOOSE) {
133                        options.add(JSON.CHANGE_CABOOSE);
134                    } else if ((train.getThirdLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES) {
135                        options.add(JSON.CHANGE_ENGINES);
136                    }
137                }
138                if (routeLocation == train.getThirdLegEndRouteLocation()) {
139                    options.add(JSON.ADD_HELPERS);
140                }
141                jsonLocation.set(JSON.OPTIONS, options);
142            }
143
144            ObjectNode engines = this.mapper.createObjectNode();
145            engines.set(JSON.ADD, pickupEngines(engineList, routeLocation));
146            engines.set(JSON.REMOVE, dropEngines(engineList, routeLocation));
147            jsonLocation.set(JSON.ENGINES, engines);
148
149            // block cars by destination
150            // caboose or FRED is placed at end of the train
151            // passenger cars are already blocked in the car list
152            // passenger cars with negative block numbers are placed at
153            // the front of the train, positive numbers at the end of
154            // the train.
155            ArrayNode pickups = this.mapper.createArrayNode();
156            for (RouteLocation destination : route) {
157                for (Car car : carList) {
158                    if (isNextCar(car, routeLocation, destination)) {
159                        pickups.add(this.utilities.getCar(car, locale));
160                    }
161                }
162            }
163            jsonCars.set(JSON.ADD, pickups);
164            // car set outs
165            ArrayNode setouts = this.mapper.createArrayNode();
166            for (Car car : carList) {
167                if (car.getRouteDestination() == routeLocation) {
168                    setouts.add(this.utilities.getCar(car, locale));
169                }
170            }
171            jsonCars.set(JSON.REMOVE, setouts);
172
173            jsonLocation.set(JSON.TRACK, this.getTrackComments(routeLocation, carList));
174
175            if (routeLocation != train.getTrainTerminatesRouteLocation()) {
176                jsonLocation.put(JSON.TRAIN_DIRECTION, routeLocation.getTrainDirection());
177                ObjectNode length = this.mapper.createObjectNode();
178                length.put(JSON.LENGTH, train.getTrainLength(routeLocation));
179                length.put(JSON.UNIT, Setup.getLengthUnit());
180                jsonLocation.set(JSON.LENGTH, length);
181                jsonLocation.put(JSON.WEIGHT, train.getTrainWeight(routeLocation));
182                int cars = train.getNumberCarsInTrain(routeLocation);
183                int emptyCars = train.getNumberEmptyCarsInTrain(routeLocation);
184                jsonCars.put(JSON.TOTAL, cars);
185                jsonCars.put(JSON.LOADS, cars - emptyCars);
186                jsonCars.put(JSON.EMPTIES, emptyCars);
187            } else {
188                log.debug("Train terminates in {}", locationName);
189                jsonLocation.put("TrainTerminatesIn", StringEscapeUtils.escapeHtml4(locationName));
190            }
191            jsonLocation.set(JsonOperations.CARS, jsonCars);
192            locations.add(jsonLocation);
193        }
194        return locations;
195    }
196
197    protected ArrayNode dropEngines(List<Engine> engines, RouteLocation routeLocation) {
198        ArrayNode node = this.mapper.createArrayNode();
199        for (Engine engine : engines) {
200            if (engine.getRouteDestination() != null && engine.getRouteDestination().equals(routeLocation)) {
201                node.add(this.utilities.getEngine(engine, locale));
202            }
203        }
204        return node;
205    }
206
207    protected ArrayNode pickupEngines(List<Engine> engines, RouteLocation routeLocation) {
208        ArrayNode node = this.mapper.createArrayNode();
209        for (Engine engine : engines) {
210            if (engine.getRouteLocation() != null && engine.getRouteLocation().equals(routeLocation)) {
211                node.add(this.utilities.getEngine(engine, locale));
212            }
213        }
214        return node;
215    }
216
217    // TODO: migrate comments into actual setout/pickup track location spaces
218    private ObjectNode getTrackComments(RouteLocation routeLocation, List<Car> cars) {
219        ObjectNode comments = this.mapper.createObjectNode();
220        if (routeLocation.getLocation() != null) {
221            List<Track> tracks = routeLocation.getLocation().getTracksByNameList(null);
222            for (Track track : tracks) {
223                ObjectNode jsonTrack = this.mapper.createObjectNode();
224                    jsonTrack.put(JSON.ADD, StringEscapeUtils.escapeHtml4(track.getCommentPickupWithColor()));
225                    jsonTrack.put(JSON.REMOVE, StringEscapeUtils.escapeHtml4(track.getCommentSetoutWithColor()));
226                    jsonTrack.put(JSON.ADD_AND_REMOVE, StringEscapeUtils.escapeHtml4(track.getCommentBothWithColor()));
227                    jsonTrack.put(JSON.COMMENT, StringEscapeUtils.escapeHtml4(track.getComment()));
228                    comments.set(track.getId(), jsonTrack);
229            }
230        }
231        return comments;
232    }
233}