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