001package jmri.jmrit.operations.routes;
002
003import java.util.ArrayList;
004import java.util.Enumeration;
005import java.util.Hashtable;
006import java.util.List;
007
008import javax.swing.JComboBox;
009
010import org.jdom2.Attribute;
011import org.jdom2.Element;
012import org.slf4j.Logger;
013import org.slf4j.LoggerFactory;
014
015import jmri.InstanceManager;
016import jmri.beans.PropertyChangeSupport;
017import jmri.jmrit.operations.locations.Location;
018import jmri.jmrit.operations.setup.Control;
019import jmri.jmrit.operations.setup.Setup;
020import jmri.jmrit.operations.trains.Train;
021import jmri.jmrit.operations.trains.TrainManager;
022
023/**
024 * Represents a route on the layout
025 *
026 * @author Daniel Boudreau Copyright (C) 2008, 2010
027 */
028public class Route extends PropertyChangeSupport implements java.beans.PropertyChangeListener {
029
030    public static final String NONE = "";
031
032    protected String _id = NONE;
033    protected String _name = NONE;
034    protected String _comment = NONE;
035
036    // stores location names for this route
037    protected Hashtable<String, RouteLocation> _routeHashTable = new Hashtable<>();
038    protected int _IdNumber = 0; // each location in a route gets its own id
039    protected int _sequenceNum = 0; // each location has a unique sequence number
040
041    public static final int EAST = 1; // train direction
042    public static final int WEST = 2;
043    public static final int NORTH = 4;
044    public static final int SOUTH = 8;
045
046    public static final String LISTCHANGE_CHANGED_PROPERTY = "routeListChange"; // NOI18N
047    public static final String ROUTE_STATUS_CHANGED_PROPERTY = "routeStatusChange"; // NOI18N
048    public static final String ROUTE_BLOCKING_CHANGED_PROPERTY = "routeBlockingChange"; // NOI18N
049    public static final String DISPOSE = "routeDispose"; // NOI18N
050
051    public static final String OKAY = Bundle.getMessage("ButtonOK");
052    public static final String TRAIN_BUILT = Bundle.getMessage("TrainBuilt");
053    public static final String ORPHAN = Bundle.getMessage("Orphan");
054    public static final String ERROR = Bundle.getMessage("ErrorTitle");
055
056    public static final int START = 1; // add location at start of route
057
058    public Route(String id, String name) {
059        log.debug("New route ({}) id: {}", name, id);
060        _name = name;
061        _id = id;
062    }
063
064    public String getId() {
065        return _id;
066    }
067
068    public void setName(String name) {
069        String old = _name;
070        _name = name;
071        if (!old.equals(name)) {
072            setDirtyAndFirePropertyChange("nameChange", old, name); // NOI18N
073        }
074    }
075
076    // for combo boxes
077    @Override
078    public String toString() {
079        return _name;
080    }
081
082    public String getName() {
083        return _name;
084    }
085
086    public void setComment(String comment) {
087        String old = _comment;
088        _comment = comment;
089        if (!old.equals(comment)) {
090            setDirtyAndFirePropertyChange("commentChange", old, comment); // NOI18N
091        }
092    }
093
094    public String getComment() {
095        return _comment;
096    }
097
098    public void dispose() {
099        removeTrainListeners();
100        setDirtyAndFirePropertyChange(DISPOSE, null, DISPOSE);
101    }
102
103    /**
104     * Adds a location to the end of this route
105     * 
106     * @param location The Location.
107     *
108     * @return RouteLocation created for the location added
109     */
110    public RouteLocation addLocation(Location location) {
111        _IdNumber++;
112        _sequenceNum++;
113        String id = _id + "r" + Integer.toString(_IdNumber);
114        log.debug("adding new location to ({}) id: {}", getName(), id);
115        RouteLocation rl = new RouteLocation(id, location);
116        rl.setSequenceNumber(_sequenceNum);
117        Integer old = Integer.valueOf(_routeHashTable.size());
118        _routeHashTable.put(rl.getId(), rl);
119
120        resetBlockingOrder();
121        setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, old, Integer.valueOf(_routeHashTable.size()));
122        // listen for drop and pick up changes to forward
123        rl.addPropertyChangeListener(this);
124        return rl;
125    }
126
127    /**
128     * Add a location at a specific place (sequence) in the route Allowable sequence
129     * numbers are 1 to max size of route. 1 = start of route, or Route.START
130     * 
131     * @param location The Location to add.
132     * @param sequence Where in the route to add the location.
133     *
134     * @return route location
135     */
136    public RouteLocation addLocation(Location location, int sequence) {
137        RouteLocation rl = addLocation(location);
138        if (sequence < 1 || sequence > _routeHashTable.size()) {
139            return rl;
140        }
141        for (int i = 0; i < _routeHashTable.size() - sequence; i++) {
142            moveLocationUp(rl);
143        }
144        return rl;
145    }
146
147    /**
148     * Remember a NamedBean Object created outside the manager.
149     * 
150     * @param rl The RouteLocation to add to this route.
151     */
152    public void register(RouteLocation rl) {
153        Integer old = Integer.valueOf(_routeHashTable.size());
154        _routeHashTable.put(rl.getId(), rl);
155
156        // find last id created
157        String[] getId = rl.getId().split("r");
158        int id = Integer.parseInt(getId[1]);
159        if (id > _IdNumber) {
160            _IdNumber = id;
161        }
162        // find and save the highest sequence number
163        if (rl.getSequenceNumber() > _sequenceNum) {
164            _sequenceNum = rl.getSequenceNumber();
165        }
166        setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, old, Integer.valueOf(_routeHashTable.size()));
167        // listen for drop and pick up changes to forward
168        rl.addPropertyChangeListener(this);
169    }
170
171    /**
172     * Delete a RouteLocation
173     * 
174     * @param rl The RouteLocation to remove from the route.
175     *
176     */
177    public void deleteLocation(RouteLocation rl) {
178        if (rl != null) {
179            rl.removePropertyChangeListener(this);
180            String id = rl.getId();
181            rl.dispose();
182            Integer old = Integer.valueOf(_routeHashTable.size());
183            _routeHashTable.remove(id);
184            resequence();
185            resetBlockingOrder();
186            setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, old, Integer.valueOf(_routeHashTable.size()));
187        }
188    }
189
190    public int size() {
191        return _routeHashTable.size();
192    }
193
194    /**
195     * Reorder the location sequence numbers for this route
196     */
197    private void resequence() {
198        List<RouteLocation> routeList = getLocationsBySequenceList();
199        for (int i = 0; i < routeList.size(); i++) {
200            _sequenceNum = i + 1; // start sequence numbers at 1
201            routeList.get(i).setSequenceNumber(_sequenceNum);
202        }
203    }
204
205    /**
206     * Get the first location in a route
207     *
208     * @return the first route location
209     */
210    public RouteLocation getDepartsRouteLocation() {
211        List<RouteLocation> list = getLocationsBySequenceList();
212        if (list.size() > 0) {
213            return list.get(0);
214        }
215        return null;
216    }
217
218    public String getDepartureDirection() {
219        if (getDepartsRouteLocation() != null) {
220            return getDepartsRouteLocation().getTrainDirectionString();
221        }
222        return NONE;
223    }
224
225    /**
226     * Get the last location in a route
227     *
228     * @return the last route location
229     */
230    public RouteLocation getTerminatesRouteLocation() {
231        List<RouteLocation> list = getLocationsBySequenceList();
232        if (list.size() > 0) {
233            return list.get(list.size() - 1);
234        }
235        return null;
236    }
237
238    /**
239     * Gets the next route location in a route
240     *
241     * @param rl the current route location
242     * @return the next route location, null if rl is the last location in a route.
243     */
244    public RouteLocation getNextRouteLocation(RouteLocation rl) {
245        List<RouteLocation> list = getLocationsBySequenceList();
246        for (int i = 0; i < list.size() - 1; i++) {
247            if (rl == list.get(i)) {
248                return list.get(i + 1);
249            }
250        }
251        return null;
252    }
253
254    /**
255     * Get location by name (gets last route location with name)
256     * 
257     * @param name The string location name.
258     *
259     * @return route location
260     */
261    public RouteLocation getLastLocationByName(String name) {
262        List<RouteLocation> routeList = getLocationsBySequenceList();
263        RouteLocation rl;
264
265        for (int i = routeList.size() - 1; i >= 0; i--) {
266            rl = routeList.get(i);
267            if (rl.getName().equals(name)) {
268                return rl;
269            }
270        }
271        return null;
272    }
273
274    /**
275     * Get a RouteLocation by id
276     * 
277     * @param id The string id.
278     *
279     * @return route location
280     */
281    public RouteLocation getLocationById(String id) {
282        return _routeHashTable.get(id);
283    }
284
285    private List<RouteLocation> getLocationsByIdList() {
286        List<RouteLocation> out = new ArrayList<>();
287        Enumeration<RouteLocation> en = _routeHashTable.elements();
288        while (en.hasMoreElements()) {
289            out.add(en.nextElement());
290        }
291        return out;
292    }
293
294    /**
295     * Get a list of RouteLocations sorted by route order
296     *
297     * @return list of RouteLocations ordered by sequence
298     */
299    public List<RouteLocation> getLocationsBySequenceList() {
300        // now re-sort
301        List<RouteLocation> out = new ArrayList<>();
302        for (RouteLocation rl : getLocationsByIdList()) {
303            for (int j = 0; j < out.size(); j++) {
304                if (rl.getSequenceNumber() < out.get(j).getSequenceNumber()) {
305                    out.add(j, rl);
306                    break;
307                }
308            }
309            if (!out.contains(rl)) {
310                out.add(rl);
311            }
312        }
313        return out;
314    }
315
316    public List<RouteLocation> getBlockingOrder() {
317        // now re-sort
318        List<RouteLocation> out = new ArrayList<>();
319        for (RouteLocation rl : getLocationsBySequenceList()) {
320            if (rl.getBlockingOrder() == 0) {
321                rl.setBlockingOrder(out.size() + 1);
322            }
323            for (int j = 0; j < out.size(); j++) {
324                if (rl.getBlockingOrder() < out.get(j).getBlockingOrder()) {
325                    out.add(j, rl);
326                    break;
327                }
328            }
329            if (!out.contains(rl)) {
330                out.add(rl);
331            }
332        }
333        return out;
334    }
335
336    public void setBlockingOrderUp(RouteLocation rl) {
337        List<RouteLocation> blockingOrder = getBlockingOrder();
338        int order = rl.getBlockingOrder();
339        if (--order < 1) {
340            order = size();
341            for (RouteLocation rlx : blockingOrder) {
342                rlx.setBlockingOrder(rlx.getBlockingOrder() - 1);
343            }
344        } else {
345            RouteLocation rlx = blockingOrder.get(order - 1);
346            rlx.setBlockingOrder(order + 1);
347        }
348        rl.setBlockingOrder(order);
349        setDirtyAndFirePropertyChange(ROUTE_BLOCKING_CHANGED_PROPERTY, order + 1, order);
350    }
351
352    public void setBlockingOrderDown(RouteLocation rl) {
353        List<RouteLocation> blockingOrder = getBlockingOrder();
354        int order = rl.getBlockingOrder();
355        if (++order > size()) {
356            order = 1;
357            for (RouteLocation rlx : blockingOrder) {
358                rlx.setBlockingOrder(rlx.getBlockingOrder() + 1);
359            }
360        } else {
361            RouteLocation rlx = blockingOrder.get(order - 1);
362            rlx.setBlockingOrder(order - 1);
363        }
364        rl.setBlockingOrder(order);
365        setDirtyAndFirePropertyChange(ROUTE_BLOCKING_CHANGED_PROPERTY, order - 1, order);
366    }
367
368    public void resetBlockingOrder() {
369        for (RouteLocation rl : getLocationsByIdList()) {
370            rl.setBlockingOrder(0);
371        }
372        setDirtyAndFirePropertyChange(ROUTE_BLOCKING_CHANGED_PROPERTY, "Order", "Reset");
373    }
374
375    /**
376     * Places a RouteLocation earlier in the route.
377     * 
378     * @param rl The RouteLocation to move.
379     *
380     */
381    public void moveLocationUp(RouteLocation rl) {
382        int sequenceNum = rl.getSequenceNumber();
383        if (sequenceNum - 1 <= 0) {
384            rl.setSequenceNumber(_sequenceNum + 1); // move to the end of the list
385            resequence();
386        } else {
387            // adjust the other item taken by this one
388            RouteLocation replaceRl = getRouteLocationBySequenceNumber(sequenceNum - 1);
389            if (replaceRl != null) {
390                replaceRl.setSequenceNumber(sequenceNum);
391                rl.setSequenceNumber(sequenceNum - 1);
392            } else {
393                resequence(); // error the sequence number is missing
394            }
395        }
396        resetBlockingOrder();
397        setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, null, Integer.toString(sequenceNum));
398    }
399
400    /**
401     * Moves a RouteLocation later in the route.
402     * 
403     * @param rl The RouteLocation to move.
404     *
405     */
406    public void moveLocationDown(RouteLocation rl) {
407        int sequenceNum = rl.getSequenceNumber();
408        if (sequenceNum + 1 > _sequenceNum) {
409            rl.setSequenceNumber(0); // move to the start of the list
410            resequence();
411        } else {
412            // adjust the other item taken by this one
413            RouteLocation replaceRl = getRouteLocationBySequenceNumber(sequenceNum + 1);
414            if (replaceRl != null) {
415                replaceRl.setSequenceNumber(sequenceNum);
416                rl.setSequenceNumber(sequenceNum + 1);
417            } else {
418                resequence(); // error the sequence number is missing
419            }
420        }
421        resetBlockingOrder();
422        setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, null, Integer.toString(sequenceNum));
423    }
424
425    /**
426     * 1st RouteLocation in a route starts at 1.
427     * 
428     * @param sequence selects which RouteLocation is to be returned
429     * @return RouteLocation selected
430     */
431    public RouteLocation getRouteLocationBySequenceNumber(int sequence) {
432        for (RouteLocation rl : getLocationsByIdList()) {
433            if (rl.getSequenceNumber() == sequence) {
434                return rl;
435            }
436        }
437        return null;
438    }
439
440    /**
441     * Gets the status of the route: OKAY ORPHAN ERROR TRAIN_BUILT
442     *
443     * @return string with status of route.
444     */
445    public String getStatus() {
446        removeTrainListeners();
447        addTrainListeners(); // and add them right back in
448        List<RouteLocation> routeList = getLocationsByIdList();
449        if (routeList.size() == 0) {
450            return ERROR;
451        }
452        List<String> directions = Setup.getTrainDirectionList();
453        for (RouteLocation rl : routeList) {
454            if (rl.getName().equals(RouteLocation.DELETED)) {
455                return ERROR;
456            }
457            // did user eliminate the train direction for this route location?
458            if (!directions.contains(rl.getTrainDirectionString())) {
459                return ERROR;
460            }
461        }
462        // check to see if this route is used by a train that is built
463        for (Train train : InstanceManager.getDefault(TrainManager.class).getTrainsByIdList()) {
464            if (train.getRoute() == this && train.isBuilt()) {
465                return TRAIN_BUILT;
466            }
467        }
468        // check to see if this route is used by a train
469        for (Train train : InstanceManager.getDefault(TrainManager.class).getTrainsByIdList()) {
470            if (train.getRoute() == this) {
471                return OKAY;
472            }
473        }
474        return ORPHAN;
475    }
476
477    private void addTrainListeners() {
478        for (Train train : InstanceManager.getDefault(TrainManager.class).getTrainsByIdList()) {
479            if (train.getRoute() == this) {
480                train.addPropertyChangeListener(this);
481            }
482        }
483    }
484
485    private void removeTrainListeners() {
486        for (Train train : InstanceManager.getDefault(TrainManager.class).getTrainsByIdList()) {
487            train.removePropertyChangeListener(this);
488        }
489    }
490
491    /**
492     * Gets the shortest train length specified in the route.
493     * 
494     * @return the minimum scale train length for this route.
495     */
496    public int getRouteMinimumTrainLength() {
497        int min = getRouteMaximumTrainLength();
498        for (RouteLocation rl : getLocationsByIdList()) {
499            if (rl.getMaxTrainLength() < min)
500                min = rl.getMaxTrainLength();
501        }
502        return min;
503    }
504
505    /**
506     * Gets the longest train length specified in the route.
507     * 
508     * @return the maximum scale train length for this route.
509     */
510    public int getRouteMaximumTrainLength() {
511        int max = 0;
512        for (RouteLocation rl : getLocationsByIdList()) {
513            if (rl.getMaxTrainLength() > max)
514                max = rl.getMaxTrainLength();
515        }
516        return max;
517    }
518
519    public JComboBox<RouteLocation> getComboBox() {
520        JComboBox<RouteLocation> box = new JComboBox<>();
521        for (RouteLocation rl : getLocationsBySequenceList()) {
522            box.addItem(rl);
523        }
524        return box;
525    }
526
527    public void updateComboBox(JComboBox<RouteLocation> box) {
528        box.removeAllItems();
529        box.addItem(null);
530        for (RouteLocation rl : getLocationsBySequenceList()) {
531            box.addItem(rl);
532        }
533    }
534
535    /**
536     * Construct this Entry from XML. This member has to remain synchronized with
537     * the detailed DTD in operations-config.xml
538     *
539     * @param e Consist XML element
540     */
541    public Route(Element e) {
542        Attribute a;
543        if ((a = e.getAttribute(Xml.ID)) != null) {
544            _id = a.getValue();
545        } else {
546            log.warn("no id attribute in route element when reading operations");
547        }
548        if ((a = e.getAttribute(Xml.NAME)) != null) {
549            _name = a.getValue();
550        }
551        if ((a = e.getAttribute(Xml.COMMENT)) != null) {
552            _comment = a.getValue();
553        }
554        if (e.getChildren(Xml.LOCATION) != null) {
555            List<Element> eRouteLocations = e.getChildren(Xml.LOCATION);
556            log.debug("route: ({}) has {} locations", getName(), eRouteLocations.size());
557            for (Element eRouteLocation : eRouteLocations) {
558                register(new RouteLocation(eRouteLocation));
559            }
560        }
561    }
562
563    /**
564     * Create an XML element to represent this Entry. This member has to remain
565     * synchronized with the detailed DTD in operations-config.xml.
566     *
567     * @return Contents in a JDOM Element
568     */
569    public Element store() {
570        Element e = new Element(Xml.ROUTE);
571        e.setAttribute(Xml.ID, getId());
572        e.setAttribute(Xml.NAME, getName());
573        e.setAttribute(Xml.COMMENT, getComment());
574        for (RouteLocation rl : getLocationsBySequenceList()) {
575            e.addContent(rl.store());
576        }
577        return e;
578    }
579
580    @Override
581    public void propertyChange(java.beans.PropertyChangeEvent e) {
582        if (Control.SHOW_PROPERTY) {
583            log.debug("Property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e.getOldValue(),
584                    e.getNewValue());
585        }
586        // forward drops, pick ups, train direction, max moves, and max length as a list
587        // change
588        if (e.getPropertyName().equals(RouteLocation.DROP_CHANGED_PROPERTY) ||
589                e.getPropertyName().equals(RouteLocation.PICKUP_CHANGED_PROPERTY) ||
590                e.getPropertyName().equals(RouteLocation.TRAIN_DIRECTION_CHANGED_PROPERTY) ||
591                e.getPropertyName().equals(RouteLocation.MAX_MOVES_CHANGED_PROPERTY) ||
592                e.getPropertyName().equals(RouteLocation.MAX_LENGTH_CHANGED_PROPERTY)) {
593            setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, null, "RouteLocation"); // NOI18N
594        }
595        if (e.getPropertyName().equals(Train.BUILT_CHANGED_PROPERTY)) {
596            firePropertyChange(ROUTE_STATUS_CHANGED_PROPERTY, true, false);
597        }
598    }
599
600    protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
601        InstanceManager.getDefault(RouteManagerXml.class).setDirty(true);
602        firePropertyChange(p, old, n);
603    }
604
605    private final static Logger log = LoggerFactory.getLogger(Route.class);
606
607}