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