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