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