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,
103                    StringEscapeUtils.escapeHtml4(routeLocation.getLocation().getCommentWithColor()));
104            locationNode.put(JSON.NAME, routeLocation.getLocation().getId());
105            jsonLocation.set(JsonOperations.LOCATION, locationNode);
106            jsonLocation.put(JSON.COMMENT, StringEscapeUtils.escapeHtml4(routeLocation.getCommentWithColor()));
107            // engine change or helper service?
108            if (train.getSecondLegOptions() != Train.NO_CABOOSE_OR_FRED) {
109                ArrayNode options = this.mapper.createArrayNode();
110                if (routeLocation == train.getSecondLegStartRouteLocation()) {
111                    if ((train.getSecondLegOptions() & Train.HELPER_ENGINES) == Train.HELPER_ENGINES) {
112                        options.add(JSON.ADD_HELPERS);
113                    } else if ((train.getSecondLegOptions() & Train.REMOVE_CABOOSE) == Train.REMOVE_CABOOSE
114                            || (train.getSecondLegOptions() & Train.ADD_CABOOSE) == Train.ADD_CABOOSE) {
115                        options.add(JSON.CHANGE_CABOOSE);
116                    } else if ((train.getSecondLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES) {
117                        options.add(JSON.CHANGE_ENGINES);
118                    }
119                }
120                if (routeLocation == train.getSecondLegEndRouteLocation()) {
121                    options.add(JSON.REMOVE_HELPERS);
122                }
123                jsonLocation.set(JSON.OPTIONS, options);
124            }
125            if (train.getThirdLegOptions() != Train.NO_CABOOSE_OR_FRED) {
126                ArrayNode options = this.mapper.createArrayNode();
127                if (routeLocation == train.getThirdLegStartRouteLocation()) {
128                    if ((train.getThirdLegOptions() & Train.HELPER_ENGINES) == Train.HELPER_ENGINES) {
129                        options.add(JSON.ADD_HELPERS);
130                    } else if ((train.getThirdLegOptions() & Train.REMOVE_CABOOSE) == Train.REMOVE_CABOOSE
131                            || (train.getThirdLegOptions() & Train.ADD_CABOOSE) == Train.ADD_CABOOSE) {
132                        options.add(JSON.CHANGE_CABOOSE);
133                    } else if ((train.getThirdLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES) {
134                        options.add(JSON.CHANGE_ENGINES);
135                    }
136                }
137                if (routeLocation == train.getThirdLegEndRouteLocation()) {
138                    options.add(JSON.ADD_HELPERS);
139                }
140                jsonLocation.set(JSON.OPTIONS, options);
141            }
142
143            ObjectNode engines = this.mapper.createObjectNode();
144            engines.set(JSON.ADD, pickupEngines(engineList, routeLocation));
145            engines.set(JSON.REMOVE, dropEngines(engineList, routeLocation));
146            jsonLocation.set(JSON.ENGINES, engines);
147
148            // block cars by destination
149            // caboose or FRED is placed at end of the train
150            // passenger cars are already blocked in the car list
151            // passenger cars with negative block numbers are placed at
152            // the front of the train, positive numbers at the end of
153            // the train.
154            ArrayNode pickups = this.mapper.createArrayNode();
155            for (RouteLocation destination : route) {
156                for (Car car : carList) {
157                    if (isNextCar(car, routeLocation, destination)) {
158                        pickups.add(this.utilities.getCar(car, locale));
159                    }
160                }
161            }
162            jsonCars.set(JSON.ADD, pickups);
163            // car set outs
164            ArrayNode setouts = this.mapper.createArrayNode();
165            for (Car car : carList) {
166                if (car.getRouteDestination() == routeLocation) {
167                    setouts.add(this.utilities.getCar(car, locale));
168                }
169            }
170            jsonCars.set(JSON.REMOVE, setouts);
171
172            jsonLocation.set(JsonOperations.TRACK, this.getTrackComments(routeLocation, carList));
173
174            if (routeLocation != train.getTrainTerminatesRouteLocation()) {
175                jsonLocation.put(JSON.TRAIN_DIRECTION, routeLocation.getTrainDirection());
176                ObjectNode length = this.mapper.createObjectNode();
177                length.put(JSON.LENGTH, train.getTrainLength(routeLocation));
178                length.put(JSON.UNIT, Setup.getLengthUnit());
179                jsonLocation.set(JSON.LENGTH, length);
180                jsonLocation.put(JsonOperations.WEIGHT, train.getTrainWeight(routeLocation));
181                int cars = train.getNumberCarsInTrain(routeLocation);
182                int emptyCars = train.getNumberEmptyCarsInTrain(routeLocation);
183                jsonCars.put(JSON.TOTAL, cars);
184                jsonCars.put(JSON.LOADS, cars - emptyCars);
185                jsonCars.put(JSON.EMPTIES, emptyCars);
186            } else {
187                log.debug("Train terminates in {}", locationName);
188                jsonLocation.put("TrainTerminatesIn", StringEscapeUtils.escapeHtml4(locationName));
189            }
190            jsonLocation.set(JsonOperations.CARS, jsonCars);
191            locations.add(jsonLocation);
192        }
193        return locations;
194    }
195
196    protected ArrayNode dropEngines(List<Engine> engines, RouteLocation routeLocation) {
197        ArrayNode node = this.mapper.createArrayNode();
198        for (Engine engine : engines) {
199            if (engine.getRouteDestination() != null && engine.getRouteDestination().equals(routeLocation)) {
200                node.add(this.utilities.getEngine(engine, locale));
201            }
202        }
203        return node;
204    }
205
206    protected ArrayNode pickupEngines(List<Engine> engines, RouteLocation routeLocation) {
207        ArrayNode node = this.mapper.createArrayNode();
208        for (Engine engine : engines) {
209            if (engine.getRouteLocation() != null && engine.getRouteLocation().equals(routeLocation)) {
210                node.add(this.utilities.getEngine(engine, locale));
211            }
212        }
213        return node;
214    }
215
216    // TODO: migrate comments into actual setout/pickup track location spaces
217    private ObjectNode getTrackComments(RouteLocation routeLocation, List<Car> cars) {
218        ObjectNode comments = this.mapper.createObjectNode();
219        if (routeLocation.getLocation() != null) {
220            List<Track> tracks = routeLocation.getLocation().getTracksByNameList(null);
221            for (Track track : tracks) {
222                ObjectNode jsonTrack = this.mapper.createObjectNode();
223                    jsonTrack.put(JSON.ADD, StringEscapeUtils.escapeHtml4(track.getCommentPickupWithColor()));
224                    jsonTrack.put(JSON.REMOVE, StringEscapeUtils.escapeHtml4(track.getCommentSetoutWithColor()));
225                    jsonTrack.put(JSON.ADD_AND_REMOVE, StringEscapeUtils.escapeHtml4(track.getCommentBothWithColor()));
226                    jsonTrack.put(JSON.COMMENT, StringEscapeUtils.escapeHtml4(track.getComment()));
227                    comments.set(track.getId(), jsonTrack);
228            }
229        }
230        return comments;
231    }
232}