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 void setBlockingOrderUp(RouteLocation rl) {
352        List<RouteLocation> blockingOrder = getBlockingOrder();
353        int order = rl.getBlockingOrder();
354        if (--order < 1) {
355            order = size();
356            for (RouteLocation rlx : blockingOrder) {
357                rlx.setBlockingOrder(rlx.getBlockingOrder() - 1);
358            }
359        } else {
360            RouteLocation rlx = blockingOrder.get(order - 1);
361            rlx.setBlockingOrder(order + 1);
362        }
363        rl.setBlockingOrder(order);
364        setDirtyAndFirePropertyChange(ROUTE_BLOCKING_CHANGED_PROPERTY, order + 1, order);
365    }
366
367    public void setBlockingOrderDown(RouteLocation rl) {
368        List<RouteLocation> blockingOrder = getBlockingOrder();
369        int order = rl.getBlockingOrder();
370        if (++order > size()) {
371            order = 1;
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 resetBlockingOrder() {
384        for (RouteLocation rl : getLocationsByIdList()) {
385            rl.setBlockingOrder(0);
386        }
387        setDirtyAndFirePropertyChange(ROUTE_BLOCKING_CHANGED_PROPERTY, "Order", "Reset");
388    }
389
390    /**
391     * Places a RouteLocation earlier in the route.
392     * 
393     * @param rl The RouteLocation to move.
394     *
395     */
396    public void moveLocationUp(RouteLocation rl) {
397        int sequenceNum = rl.getSequenceNumber();
398        if (sequenceNum - 1 <= 0) {
399            rl.setSequenceNumber(_sequenceNum + 1); // move to the end of the list
400            resequence();
401        } else {
402            // adjust the other item taken by this one
403            RouteLocation replaceRl = getRouteLocationBySequenceNumber(sequenceNum - 1);
404            if (replaceRl != null) {
405                replaceRl.setSequenceNumber(sequenceNum);
406                rl.setSequenceNumber(sequenceNum - 1);
407            } else {
408                resequence(); // error the sequence number is missing
409            }
410        }
411        resetBlockingOrder();
412        setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, null, Integer.toString(sequenceNum));
413    }
414
415    /**
416     * Moves a RouteLocation later in the route.
417     * 
418     * @param rl The RouteLocation to move.
419     *
420     */
421    public void moveLocationDown(RouteLocation rl) {
422        int sequenceNum = rl.getSequenceNumber();
423        if (sequenceNum + 1 > _sequenceNum) {
424            rl.setSequenceNumber(0); // move to the start of the list
425            resequence();
426        } else {
427            // adjust the other item taken by this one
428            RouteLocation replaceRl = getRouteLocationBySequenceNumber(sequenceNum + 1);
429            if (replaceRl != null) {
430                replaceRl.setSequenceNumber(sequenceNum);
431                rl.setSequenceNumber(sequenceNum + 1);
432            } else {
433                resequence(); // error the sequence number is missing
434            }
435        }
436        resetBlockingOrder();
437        setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, null, Integer.toString(sequenceNum));
438    }
439
440    /**
441     * 1st RouteLocation in a route starts at 1.
442     * 
443     * @param sequence selects which RouteLocation is to be returned
444     * @return RouteLocation selected
445     */
446    public RouteLocation getRouteLocationBySequenceNumber(int sequence) {
447        for (RouteLocation rl : getLocationsByIdList()) {
448            if (rl.getSequenceNumber() == sequence) {
449                return rl;
450            }
451        }
452        return null;
453    }
454
455    /**
456     * Gets the status of the route: OKAY ORPHAN ERROR TRAIN_BUILT
457     *
458     * @return string with status of route.
459     */
460    public String getStatus() {
461        removeTrainListeners();
462        addTrainListeners(); // and add them right back in
463        List<RouteLocation> routeList = getLocationsByIdList();
464        if (routeList.size() == 0) {
465            return ERROR;
466        }
467        List<String> directions = Setup.getTrainDirectionList();
468        for (RouteLocation rl : routeList) {
469            if (rl.getName().equals(RouteLocation.DELETED)) {
470                return ERROR;
471            }
472            // did user eliminate the train direction for this route location?
473            if (!directions.contains(rl.getTrainDirectionString())) {
474                return ERROR;
475            }
476        }
477        // check to see if this route is used by a train that is built
478        for (Train train : InstanceManager.getDefault(TrainManager.class).getTrainsByIdList()) {
479            if (train.getRoute() == this && train.isBuilt()) {
480                return TRAIN_BUILT;
481            }
482        }
483        // check to see if this route is used by a train
484        for (Train train : InstanceManager.getDefault(TrainManager.class).getTrainsByIdList()) {
485            if (train.getRoute() == this) {
486                return OKAY;
487            }
488        }
489        return ORPHAN;
490    }
491
492    private void addTrainListeners() {
493        for (Train train : InstanceManager.getDefault(TrainManager.class).getTrainsByIdList()) {
494            if (train.getRoute() == this) {
495                train.addPropertyChangeListener(this);
496            }
497        }
498    }
499
500    private void removeTrainListeners() {
501        for (Train train : InstanceManager.getDefault(TrainManager.class).getTrainsByIdList()) {
502            train.removePropertyChangeListener(this);
503        }
504    }
505
506    /**
507     * Gets the shortest train length specified in the route.
508     * 
509     * @return the minimum scale train length for this route.
510     */
511    public int getRouteMinimumTrainLength() {
512        int min = getRouteMaximumTrainLength();
513        for (RouteLocation rl : getLocationsByIdList()) {
514            if (rl.getMaxTrainLength() < min)
515                min = rl.getMaxTrainLength();
516        }
517        return min;
518    }
519
520    /**
521     * Gets the longest train length specified in the route.
522     * 
523     * @return the maximum scale train length for this route.
524     */
525    public int getRouteMaximumTrainLength() {
526        int max = 0;
527        for (RouteLocation rl : getLocationsByIdList()) {
528            if (rl.getMaxTrainLength() > max)
529                max = rl.getMaxTrainLength();
530        }
531        return max;
532    }
533
534    public JComboBox<RouteLocation> getComboBox() {
535        JComboBox<RouteLocation> box = new JComboBox<>();
536        for (RouteLocation rl : getLocationsBySequenceList()) {
537            box.addItem(rl);
538        }
539        return box;
540    }
541
542    public void updateComboBox(JComboBox<RouteLocation> box) {
543        box.removeAllItems();
544        box.addItem(null);
545        for (RouteLocation rl : getLocationsBySequenceList()) {
546            box.addItem(rl);
547        }
548    }
549
550    /**
551     * Construct this Entry from XML. This member has to remain synchronized with
552     * the detailed DTD in operations-config.xml
553     *
554     * @param e Consist XML element
555     */
556    public Route(Element e) {
557        Attribute a;
558        if ((a = e.getAttribute(Xml.ID)) != null) {
559            _id = a.getValue();
560        } else {
561            log.warn("no id attribute in route element when reading operations");
562        }
563        if ((a = e.getAttribute(Xml.NAME)) != null) {
564            _name = a.getValue();
565        }
566        if ((a = e.getAttribute(Xml.COMMENT)) != null) {
567            _comment = a.getValue();
568        }
569        if (e.getChildren(Xml.LOCATION) != null) {
570            List<Element> eRouteLocations = e.getChildren(Xml.LOCATION);
571            log.debug("route: ({}) has {} locations", getName(), eRouteLocations.size());
572            for (Element eRouteLocation : eRouteLocations) {
573                register(new RouteLocation(eRouteLocation));
574            }
575        }
576    }
577
578    /**
579     * Create an XML element to represent this Entry. This member has to remain
580     * synchronized with the detailed DTD in operations-config.xml.
581     *
582     * @return Contents in a JDOM Element
583     */
584    public Element store() {
585        Element e = new Element(Xml.ROUTE);
586        e.setAttribute(Xml.ID, getId());
587        e.setAttribute(Xml.NAME, getName());
588        e.setAttribute(Xml.COMMENT, getComment());
589        for (RouteLocation rl : getLocationsBySequenceList()) {
590            e.addContent(rl.store());
591        }
592        return e;
593    }
594
595    @Override
596    public void propertyChange(java.beans.PropertyChangeEvent e) {
597        if (Control.SHOW_PROPERTY) {
598            log.debug("Property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e.getOldValue(),
599                    e.getNewValue());
600        }
601        // forward drops, pick ups, local moves, train direction, max moves, and max length as a list
602        // change
603        if (e.getPropertyName().equals(RouteLocation.DROP_CHANGED_PROPERTY) ||
604                e.getPropertyName().equals(RouteLocation.PICKUP_CHANGED_PROPERTY) ||
605                e.getPropertyName().equals(RouteLocation.LOCAL_MOVES_CHANGED_PROPERTY) ||
606                e.getPropertyName().equals(RouteLocation.TRAIN_DIRECTION_CHANGED_PROPERTY) ||
607                e.getPropertyName().equals(RouteLocation.MAX_MOVES_CHANGED_PROPERTY) ||
608                e.getPropertyName().equals(RouteLocation.MAX_LENGTH_CHANGED_PROPERTY)) {
609            setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, null, "RouteLocation"); // NOI18N
610        }
611        if (e.getPropertyName().equals(Train.BUILT_CHANGED_PROPERTY)) {
612            firePropertyChange(ROUTE_STATUS_CHANGED_PROPERTY, true, false);
613        }
614    }
615
616    protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
617        InstanceManager.getDefault(RouteManagerXml.class).setDirty(true);
618        firePropertyChange(p, old, n);
619    }
620
621    private final static Logger log = LoggerFactory.getLogger(Route.class);
622
623}