001package jmri.jmrit.operations.locations;
002
003import java.awt.Point;
004import java.beans.PropertyChangeListener;
005import java.util.*;
006
007import javax.swing.JComboBox;
008
009import org.jdom2.Attribute;
010import org.jdom2.Element;
011import org.slf4j.Logger;
012import org.slf4j.LoggerFactory;
013
014import jmri.InstanceManager;
015import jmri.Reporter;
016import jmri.beans.Identifiable;
017import jmri.beans.PropertyChangeSupport;
018import jmri.jmrit.operations.OperationsPanel;
019import jmri.jmrit.operations.locations.divisions.Division;
020import jmri.jmrit.operations.locations.divisions.DivisionManager;
021import jmri.jmrit.operations.rollingstock.RollingStock;
022import jmri.jmrit.operations.rollingstock.cars.*;
023import jmri.jmrit.operations.rollingstock.engines.Engine;
024import jmri.jmrit.operations.rollingstock.engines.EngineTypes;
025import jmri.jmrit.operations.setup.Control;
026import jmri.jmrit.operations.setup.Setup;
027import jmri.jmrit.operations.trains.trainbuilder.TrainCommon;
028import jmri.util.PhysicalLocation;
029
030/**
031 * Represents a location on the layout
032 *
033 * @author Daniel Boudreau Copyright (C) 2008, 2012, 2013
034 */
035public class Location extends PropertyChangeSupport implements Identifiable, PropertyChangeListener {
036
037    public static final String LOC_TRACK_REGIX = "s";
038
039    public static final String NONE = "";
040    public static final int RANGE_DEFAULT = 25;
041
042    protected String _id = NONE; // location id
043    protected String _name = NONE;
044    protected int _IdNumber = 0; // last track id number created
045    protected int _numberRS = 0; // number of cars and engines (total rolling
046                                 // stock)
047    protected int _numberCars = 0; // number of cars
048    protected int _numberEngines = 0; // number of engines
049    protected int _pickupRS = 0;
050    protected int _dropRS = 0;
051    protected int _trainDir = EAST + WEST + NORTH + SOUTH; // train directions
052    protected int _length = 0; // length of all tracks at this location
053    protected int _usedLength = 0; // length of track filled by cars and engines
054    protected String _comment = NONE;
055    protected String _switchListComment = NONE; // optional switch list comment
056    protected boolean _switchList = true; // when true print switchlist
057    protected String _defaultPrinter = NONE; // the default printer name
058    protected String _status = UNKNOWN; // print switch list status
059    protected int _switchListState = SW_CREATE; // switch list state
060    protected Point _trainIconEast = new Point(); // coordinates east bound
061    protected Point _trainIconWest = new Point();
062    protected Point _trainIconNorth = new Point();
063    protected Point _trainIconSouth = new Point();
064    protected int _trainIconRangeX = RANGE_DEFAULT;
065    protected int _trainIconRangeY = RANGE_DEFAULT;
066    protected Hashtable<String, Track> _trackHashTable = new Hashtable<>();
067    protected PhysicalLocation _physicalLocation = new PhysicalLocation();
068    protected List<String> _listTypes = new ArrayList<>();
069    protected Division _division = null;
070
071    // IdTag reader associated with this location.
072    protected Reporter _reader = null;
073
074    // Pool
075    protected int _idPoolNumber = 0;
076    protected Hashtable<String, Pool> _poolHashTable = new Hashtable<>();
077
078    public static final String NORMAL = "1"; // types of track allowed at this
079                                             // location
080    public static final String STAGING = "2"; // staging only
081
082    public static final int EAST = 1; // train direction serviced by this
083                                      // location
084    public static final int WEST = 2;
085    public static final int NORTH = 4;
086    public static final int SOUTH = 8;
087
088    // Switch list status
089    public static final String UNKNOWN = "";
090    public static final String PRINTED = Bundle.getMessage("Printed");
091    public static final String CSV_GENERATED = Bundle.getMessage("CsvGenerated");
092    public static final String MODIFIED = Bundle.getMessage("Modified");
093    public static final String UPDATED = Bundle.getMessage("Updated");
094
095    // Switch list states
096    public static final int SW_CREATE = 0; // create new switch list
097    public static final int SW_APPEND = 1; // append train into to switch list
098    public static final int SW_PRINTED = 2; // switch list printed
099
100    // For property change
101    public static final String TRACK_LISTLENGTH_CHANGED_PROPERTY = "trackListLength"; // NOI18N
102    public static final String TYPES_CHANGED_PROPERTY = "locationTypes"; // NOI18N
103    public static final String TRAIN_DIRECTION_CHANGED_PROPERTY = "locationTrainDirection"; // NOI18N
104    public static final String LENGTH_CHANGED_PROPERTY = "locationTrackLengths"; // NOI18N
105    public static final String USEDLENGTH_CHANGED_PROPERTY = "locationUsedLength"; // NOI18N
106    public static final String NAME_CHANGED_PROPERTY = "locationName"; // NOI18N
107    public static final String SWITCHLIST_CHANGED_PROPERTY = "switchList"; // NOI18N
108    public static final String DISPOSE_CHANGED_PROPERTY = "locationDispose"; // NOI18N
109    public static final String STATUS_CHANGED_PROPERTY = "locationStatus"; // NOI18N
110    public static final String POOL_LENGTH_CHANGED_PROPERTY = "poolLengthChanged"; // NOI18N
111    public static final String SWITCHLIST_COMMENT_CHANGED_PROPERTY = "switchListComment";// NOI18N
112    public static final String TRACK_BLOCKING_ORDER_CHANGED_PROPERTY = "locationTrackBlockingOrder";// NOI18N
113    public static final String LOCATION_REPORTER_CHANGED_PROPERTY = "locationReporterChange"; // NOI18N
114    public static final String LOCATION_DIVISION_CHANGED_PROPERTY = "homeDivisionChange"; // NOI18N
115
116    public Location(String id, String name) {
117        log.debug("New location ({}) id: {}", name, id);
118        _name = name;
119        _id = id;
120        // a new location accepts all types
121        setTypeNames(InstanceManager.getDefault(CarTypes.class).getNames());
122        setTypeNames(InstanceManager.getDefault(EngineTypes.class).getNames());
123        addPropertyChangeListeners();
124    }
125
126    @Override
127    public String getId() {
128        return _id;
129    }
130
131    /**
132     * Sets the location's name.
133     * 
134     * @param name The string name for this location.
135     */
136    public void setName(String name) {
137        String old = _name;
138        _name = name;
139        if (!old.equals(name)) {
140            // recalculate max location name length for Manifests
141            InstanceManager.getDefault(LocationManager.class).resetNameLengths();
142            setDirtyAndFirePropertyChange(NAME_CHANGED_PROPERTY, old, name);
143        }
144    }
145
146    // for combo boxes
147    @Override
148    public String toString() {
149        return _name;
150    }
151
152    public String getName() {
153        return _name;
154    }
155
156    public String getSplitName() {
157        return TrainCommon.splitString(getName());
158    }
159
160    /**
161     * Makes a copy of this location.
162     *
163     * @param newLocation the location to copy to
164     */
165    public void copyLocation(Location newLocation) {
166        newLocation.setComment(getCommentWithColor());
167        newLocation.setDefaultPrinterName(getDefaultPrinterName());
168        newLocation.setSwitchListComment(getSwitchListCommentWithColor());
169        newLocation.setSwitchListEnabled(isSwitchListEnabled());
170        newLocation.setTrainDirections(getTrainDirections());
171        // TODO should we set the train icon coordinates?
172        // rolling stock serviced by this location
173        for (String type : newLocation.getTypeNames()) {
174            if (acceptsTypeName(type)) {
175                continue;
176            } else {
177                newLocation.deleteTypeName(type);
178            }
179        }
180        copyTracksLocation(newLocation);
181    }
182
183    /**
184     * Copies all of the tracks at this location. If there's a track already at
185     * the copy to location with the same name, the track is skipped.
186     *
187     * @param location the location to copy the tracks to.
188     */
189    public void copyTracksLocation(Location location) {
190        for (Track track : getTracksList()) {
191            if (location.getTrackByName(track.getName(), null) != null) {
192                continue;
193            }
194            track.copyTrack(track.getName(), location);
195        }
196    }
197
198    public PhysicalLocation getPhysicalLocation() {
199        return (_physicalLocation);
200    }
201
202    public void setPhysicalLocation(PhysicalLocation l) {
203        _physicalLocation = l;
204        InstanceManager.getDefault(LocationManagerXml.class).setDirty(true);
205    }
206
207    /**
208     * Set total length of all tracks for this location
209     * 
210     * @param length The integer sum of all tracks at this location.
211     */
212    public void setLength(int length) {
213        int old = _length;
214        _length = length;
215        if (old != length) {
216            setDirtyAndFirePropertyChange(LENGTH_CHANGED_PROPERTY, old, length);
217        }
218    }
219
220    /**
221     * @return total length of all tracks for this location
222     */
223    public int getLength() {
224        return _length;
225    }
226
227    public void setUsedLength(int length) {
228        int old = _usedLength;
229        _usedLength = length;
230        if (old != length) {
231            setDirtyAndFirePropertyChange(USEDLENGTH_CHANGED_PROPERTY, old, length);
232        }
233    }
234
235    /**
236     * @return The length of the track that is occupied by cars and engines
237     */
238    public int getUsedLength() {
239        return _usedLength;
240    }
241
242    /**
243     * Used to determine if location is setup for staging
244     *
245     * @return true if location is setup as staging
246     */
247    public boolean isStaging() {
248        return hasTrackType(Track.STAGING);
249    }
250
251    /**
252     * @return True if location has spurs
253     */
254    public boolean hasSpurs() {
255        return hasTrackType(Track.SPUR);
256    }
257
258    /**
259     * @return True if location has classification/interchange tracks
260     */
261    public boolean hasInterchanges() {
262        return hasTrackType(Track.INTERCHANGE);
263    }
264
265    /**
266     * @return True if location has yard tracks
267     */
268    public boolean hasYards() {
269        return hasTrackType(Track.YARD);
270    }
271
272    /**
273     * @param trackType The track type to check.
274     * @return True if location has the track type specified Track.INTERCHANGE
275     *         Track.YARD Track.SPUR Track.Staging
276     */
277    public boolean hasTrackType(String trackType) {
278        Track track;
279        Enumeration<Track> en = _trackHashTable.elements();
280        while (en.hasMoreElements()) {
281            track = en.nextElement();
282            if (track.getTrackType().equals(trackType)) {
283                return true;
284            }
285        }
286        return false;
287    }
288
289    /**
290     * Change all tracks at this location to type
291     * 
292     * @param type Track.INTERCHANGE Track.YARD Track.SPUR Track.Staging
293     */
294    public void changeTrackType(String type) {
295        List<Track> tracks = getTracksByNameList(null);
296        for (Track track : tracks) {
297            track.setTrackType(type);
298        }
299    }
300
301    public int getNumberOfTracks() {
302        return _trackHashTable.size();
303    }
304
305    /**
306     * Sets the train directions that this location can service. EAST means that
307     * an Eastbound train can service the location.
308     *
309     * @param direction Any combination of EAST WEST NORTH SOUTH
310     */
311    public void setTrainDirections(int direction) {
312        int old = _trainDir;
313        _trainDir = direction;
314        if (old != direction) {
315            setDirtyAndFirePropertyChange(TRAIN_DIRECTION_CHANGED_PROPERTY, old, direction);
316        }
317    }
318
319    /**
320     * Gets the train directions that this location can service. EAST means that
321     * an Eastbound train can service the location.
322     *
323     * @return Any combination of EAST WEST NORTH SOUTH
324     */
325    public int getTrainDirections() {
326        return _trainDir;
327    }
328
329    /**
330     * Sets the quantity of rolling stock for this location
331     * 
332     * @param number An integer representing the quantity of rolling stock at
333     *               this location.
334     */
335    public void setNumberRS(int number) {
336        int old = _numberRS;
337        _numberRS = number;
338        if (old != number) {
339            setDirtyAndFirePropertyChange("locationNumberRS", old, number); // NOI18N
340        }
341    }
342
343    /**
344     * Gets the number of cars and engines at this location
345     *
346     * @return number of cars at this location
347     */
348    public int getNumberRS() {
349        return _numberRS;
350    }
351
352    /**
353     * Sets the number of cars at this location
354     */
355    private void setNumberCars(int number) {
356        int old = _numberCars;
357        _numberCars = number;
358        if (old != number) {
359            setDirtyAndFirePropertyChange("locationNumberCars", old, number); // NOI18N
360        }
361    }
362
363    /**
364     * @return The number of cars at this location
365     */
366    public int getNumberCars() {
367        return _numberCars;
368    }
369
370    /**
371     * Sets the number of engines at this location
372     */
373    private void setNumberEngines(int number) {
374        int old = _numberEngines;
375        _numberEngines = number;
376        if (old != number) {
377            setDirtyAndFirePropertyChange("locationNumberEngines", old, number); // NOI18N
378        }
379    }
380
381    /**
382     * @return The number of engines at this location
383     */
384    public int getNumberEngines() {
385        return _numberEngines;
386    }
387
388    /**
389     * When true, a switchlist is desired for this location. Used for preview
390     * and printing a manifest for a single location
391     * 
392     * @param switchList When true, switch lists are enabled for this location.
393     */
394    public void setSwitchListEnabled(boolean switchList) {
395        boolean old = _switchList;
396        _switchList = switchList;
397        if (old != switchList) {
398            setDirtyAndFirePropertyChange(SWITCHLIST_CHANGED_PROPERTY, old, switchList);
399        }
400    }
401
402    /**
403     * Used to determine if switch list is needed for this location
404     *
405     * @return true if switch list needed
406     */
407    public boolean isSwitchListEnabled() {
408        return _switchList;
409    }
410
411    public void setDefaultPrinterName(String name) {
412        String old = _defaultPrinter;
413        _defaultPrinter = name;
414        if (!old.equals(name)) {
415            setDirtyAndFirePropertyChange("locationDefaultPrinter", old, name); // NOI18N
416        }
417    }
418
419    public String getDefaultPrinterName() {
420        return _defaultPrinter;
421    }
422
423    /**
424     * Sets the print status for this location's switch list
425     *
426     * @param status UNKNOWN PRINTED MODIFIED UPDATED CSV_GENERATED
427     */
428    public void setStatus(String status) {
429        String old = _status;
430        _status = status;
431        if (!old.equals(status)) {
432            setDirtyAndFirePropertyChange(STATUS_CHANGED_PROPERTY, old, status);
433        }
434    }
435
436    /**
437     * The print status for this location's switch list
438     * 
439     * @return UNKNOWN PRINTED MODIFIED UPDATED CSV_GENERATED
440     */
441    public String getStatus() {
442        return _status;
443    }
444
445    /**
446     * @param state Location.SW_CREATE Location.SW_PRINTED Location.SW_APPEND
447     */
448    public void setSwitchListState(int state) {
449        int old = _switchListState;
450        _switchListState = state;
451        if (old != state) {
452            setDirtyAndFirePropertyChange("locationSwitchListState", old, state); // NOI18N
453        }
454    }
455
456    /**
457     * Returns the state of the switch list for this location.
458     *
459     * @return Location.SW_CREATE, Location.SW_PRINTED or Location.SW_APPEND
460     */
461    public int getSwitchListState() {
462        return _switchListState;
463    }
464
465    /**
466     * Sets the train icon coordinates for an eastbound train arriving at this
467     * location.
468     *
469     * @param point The XY coordinates on the panel.
470     */
471    public void setTrainIconEast(Point point) {
472        Point old = _trainIconEast;
473        _trainIconEast = point;
474        setDirtyAndFirePropertyChange("locationTrainIconEast", old, point); // NOI18N
475    }
476
477    public Point getTrainIconEast() {
478        return _trainIconEast;
479    }
480
481    public void setTrainIconWest(Point point) {
482        Point old = _trainIconWest;
483        _trainIconWest = point;
484        setDirtyAndFirePropertyChange("locationTrainIconWest", old, point); // NOI18N
485    }
486
487    public Point getTrainIconWest() {
488        return _trainIconWest;
489    }
490
491    public void setTrainIconNorth(Point point) {
492        Point old = _trainIconNorth;
493        _trainIconNorth = point;
494        setDirtyAndFirePropertyChange("locationTrainIconNorth", old, point); // NOI18N
495    }
496
497    public Point getTrainIconNorth() {
498        return _trainIconNorth;
499    }
500
501    public void setTrainIconSouth(Point point) {
502        Point old = _trainIconSouth;
503        _trainIconSouth = point;
504        setDirtyAndFirePropertyChange("locationTrainIconSouth", old, point); // NOI18N
505    }
506
507    public Point getTrainIconSouth() {
508        return _trainIconSouth;
509    }
510
511    /**
512     * Sets the X range for detecting the manual movement of a train icon.
513     * 
514     * @param x the +/- range for detection
515     */
516    public void setTrainIconRangeX(int x) {
517        int old = _trainIconRangeX;
518        _trainIconRangeX = x;
519        if (old != x) {
520            setDirtyAndFirePropertyChange("trainIconRangeX", old, x); // NOI18N
521        }
522    }
523
524    /**
525     * Used to determine the sensitivity when a user is manually moving a train
526     * icon on a panel.
527     * 
528     * @return the x +/- range for a train icon
529     */
530    public int getTrainIconRangeX() {
531        return _trainIconRangeX;
532    }
533
534    /**
535     * Sets the Y range for detecting the manual movement of a train icon.
536     * 
537     * @param y the +/- range for detection
538     */
539    public void setTrainIconRangeY(int y) {
540        int old = _trainIconRangeY;
541        _trainIconRangeY = y;
542        if (old != y) {
543            setDirtyAndFirePropertyChange("trainIconRangeY", old, y); // NOI18N
544        }
545    }
546
547    /**
548     * Used to determine the sensitivity when a user is manually moving a train
549     * icon on a panel.
550     * 
551     * @return the y +/- range for a train icon
552     */
553    public int getTrainIconRangeY() {
554        return _trainIconRangeY;
555    }
556
557    /**
558     * Adds rolling stock to a specific location.
559     * 
560     * @param rs The RollingStock to add.
561     */
562    public void addRS(RollingStock rs) {
563        setNumberRS(getNumberRS() + 1);
564        if (rs.getClass() == Car.class) {
565            setNumberCars(getNumberCars() + 1);
566        } else if (rs.getClass() == Engine.class) {
567            setNumberEngines(getNumberEngines() + 1);
568        }
569        setUsedLength(getUsedLength() + rs.getTotalLength());
570    }
571
572    public void deleteRS(RollingStock rs) {
573        setNumberRS(getNumberRS() - 1);
574        if (rs.getClass() == Car.class) {
575            setNumberCars(getNumberCars() - 1);
576        } else if (rs.getClass() == Engine.class) {
577            setNumberEngines(getNumberEngines() - 1);
578        }
579        setUsedLength(getUsedLength() - rs.getTotalLength());
580    }
581
582    /**
583     * Increments the number of cars and or engines that will be picked up by a
584     * train at this location.
585     */
586    public void addPickupRS() {
587        int old = _pickupRS;
588        _pickupRS++;
589        setDirtyAndFirePropertyChange("locationAddPickupRS", old, _pickupRS); // NOI18N
590    }
591
592    /**
593     * Decrements the number of cars and or engines that will be picked up by a
594     * train at this location.
595     */
596    public void deletePickupRS() {
597        int old = _pickupRS;
598        _pickupRS--;
599        setDirtyAndFirePropertyChange("locationDeletePickupRS", old, _pickupRS); // NOI18N
600    }
601
602    /**
603     * Increments the number of cars and or engines that will be dropped off by
604     * trains at this location.
605     */
606    public void addDropRS() {
607        int old = _dropRS;
608        _dropRS++;
609        setDirtyAndFirePropertyChange("locationAddDropRS", old, _dropRS); // NOI18N
610    }
611
612    /**
613     * Decrements the number of cars and or engines that will be dropped off by
614     * trains at this location.
615     */
616    public void deleteDropRS() {
617        int old = _dropRS;
618        _dropRS--;
619        setDirtyAndFirePropertyChange("locationDeleteDropRS", old, _dropRS); // NOI18N
620    }
621
622    /**
623     * @return the number of cars and engines that are scheduled for pick up at
624     *         this location.
625     */
626    public int getPickupRS() {
627        return _pickupRS;
628    }
629
630    /**
631     * @return the number of cars and engines that are scheduled for drop at
632     *         this location.
633     */
634    public int getDropRS() {
635        return _dropRS;
636    }
637
638    public void setDivision(Division division) {
639        Division old = _division;
640        _division = division;
641        if (old != _division) {
642            setDirtyAndFirePropertyChange(LOCATION_DIVISION_CHANGED_PROPERTY, old, division);
643        }
644    }
645
646    /**
647     * Gets the division for this location
648     * 
649     * @return the division for this location
650     */
651    public Division getDivision() {
652        return _division;
653    }
654
655    public String getDivisionName() {
656        if (getDivision() != null) {
657            return getDivision().getName();
658        }
659        return NONE;
660    }
661
662    public String getDivisionId() {
663        if (getDivision() != null) {
664            return getDivision().getId();
665        }
666        return NONE;
667    }
668
669    public void setComment(String comment) {
670        String old = _comment;
671        _comment = comment;
672        if (!old.equals(comment)) {
673            setDirtyAndFirePropertyChange("locationComment", old, comment); // NOI18N
674        }
675    }
676
677    /**
678     * Gets the comment text without color attributes
679     * 
680     * @return the comment text
681     */
682    public String getComment() {
683        return TrainCommon.getOnlyText(getCommentWithColor());
684    }
685
686    /**
687     * Gets the comment text with optional color attributes
688     * 
689     * @return text with optional color attributes
690     */
691    public String getCommentWithColor() {
692        return _comment;
693    }
694
695    public void setSwitchListComment(String comment) {
696        String old = _switchListComment;
697        _switchListComment = comment;
698        if (!old.equals(comment)) {
699            setDirtyAndFirePropertyChange(SWITCHLIST_COMMENT_CHANGED_PROPERTY, old, comment);
700        }
701    }
702
703    /**
704     * Gets the switch list comment text without color attributes
705     * 
706     * @return the comment text
707     */
708    public String getSwitchListComment() {
709        return TrainCommon.getOnlyText(getSwitchListCommentWithColor());
710    }
711
712    /**
713     * Gets the switch list comment text with optional color attributes
714     * 
715     * @return text with optional color attributes
716     */
717    public String getSwitchListCommentWithColor() {
718        return _switchListComment;
719    }
720
721    public String[] getTypeNames() {
722        return _listTypes.toArray(new String[0]);
723    }
724
725    private void setTypeNames(String[] types) {
726        if (types.length > 0) {
727            Arrays.sort(types);
728            for (String type : types) {
729                _listTypes.add(type);
730            }
731        }
732    }
733
734    /**
735     * Adds the specific type of rolling stock to the will service list
736     *
737     * @param type of rolling stock that location will service
738     */
739    public void addTypeName(String type) {
740        // insert at start of list, sort later
741        if (type == null || _listTypes.contains(type)) {
742            return;
743        }
744        _listTypes.add(0, type);
745        log.debug("Location ({}) add rolling stock type ({})", getName(), type);
746        setDirtyAndFirePropertyChange(TYPES_CHANGED_PROPERTY, _listTypes.size() - 1, _listTypes.size());
747    }
748
749    public void deleteTypeName(String type) {
750        if (_listTypes.remove(type)) {
751            log.debug("Location ({}) delete rolling stock type ({})", getName(), type);
752            setDirtyAndFirePropertyChange(TYPES_CHANGED_PROPERTY, _listTypes.size() + 1, _listTypes.size());
753        }
754    }
755
756    public boolean acceptsTypeName(String type) {
757        return _listTypes.contains(type);
758    }
759
760    /**
761     * Adds a track to this location. Valid track types are spurs, yards,
762     * staging and interchange tracks.
763     *
764     * @param name of track
765     * @param type of track, Track.INTERCHANGE, Track.SPUR, Track.STAGING,
766     *             Track.YARD
767     * @return Track
768     */
769    public Track addTrack(String name, String type) {
770        Track track = getTrackByName(name, type);
771        if (track == null) {
772            _IdNumber++;
773            // create track id
774            String id = getId() + LOC_TRACK_REGIX + Integer.toString(_IdNumber);
775            log.debug("Adding new ({}) to ({}) track name ({}) id: {}", type, getName(), name, id);
776            track = new Track(id, name, type, this);
777            // recalculate max location name length for Manifests
778            InstanceManager.getDefault(LocationManager.class).resetNameLengths();
779            register(track);
780        }
781        resetMoves(); // give all of the tracks equal weighting
782        return track;
783    }
784
785    /**
786     * Remember a NamedBean Object created outside the manager.
787     * 
788     * @param track The Track to be loaded at this location.
789     */
790    public void register(Track track) {
791        int old = getNumberOfTracks();
792        _trackHashTable.put(track.getId(), track);
793        // add to the locations's available track length
794        setLength(getLength() + track.getLength());
795        // save last id number created
796        String[] getId = track.getId().split(LOC_TRACK_REGIX);
797        int id = Integer.parseInt(getId[1]);
798        if (id > _IdNumber) {
799            _IdNumber = id;
800        }
801        setDirtyAndFirePropertyChange(TRACK_LISTLENGTH_CHANGED_PROPERTY, old, getNumberOfTracks());
802        // listen for name and state changes to forward
803        track.addPropertyChangeListener(this);
804    }
805
806    public void deleteTrack(Track track) {
807        if (track != null) {
808            track.removePropertyChangeListener(this);
809            // subtract from the locations's available track length
810            setLength(getLength() - track.getLength());
811            track.dispose();
812            int old = getNumberOfTracks();
813            _trackHashTable.remove(track.getId());
814            setDirtyAndFirePropertyChange(TRACK_LISTLENGTH_CHANGED_PROPERTY, old, getNumberOfTracks());
815        }
816    }
817
818    /**
819     * Get track at this location by name and type. Track type can be null.
820     *
821     * @param name track's name
822     * @param type track type
823     * @return track at location
824     */
825    public Track getTrackByName(String name, String type) {
826        Track track;
827        Enumeration<Track> en = _trackHashTable.elements();
828        while (en.hasMoreElements()) {
829            track = en.nextElement();
830            if (type == null) {
831                if (track.getName().equals(name)) {
832                    return track;
833                }
834            } else if (track.getName().equals(name) && track.getTrackType().equals(type)) {
835                return track;
836            }
837        }
838        return null;
839    }
840
841    public Track getTrackById(String id) {
842        return _trackHashTable.get(id);
843    }
844
845    /**
846     * Gets a list of track ids ordered by id for this location.
847     *
848     * @return list of track ids for this location
849     */
850    public List<String> getTrackIdsByIdList() {
851        List<String> out = new ArrayList<>();
852        Enumeration<String> en = _trackHashTable.keys();
853        while (en.hasMoreElements()) {
854            out.add(en.nextElement());
855        }
856        Collections.sort(out);
857        return out;
858    }
859
860    /**
861     * Gets a sorted by id list of tracks for this location.
862     *
863     * @return Sorted list of tracks by id for this location.
864     */
865    public List<Track> getTracksByIdList() {
866        List<Track> out = new ArrayList<>();
867        List<String> trackIds = getTrackIdsByIdList();
868        for (String id : trackIds) {
869            out.add(getTrackById(id));
870        }
871        return out;
872    }
873
874    /**
875     * Gets a unsorted list of the tracks at this location.
876     *
877     * @return tracks at this location.
878     */
879    public List<Track> getTracksList() {
880        List<Track> out = new ArrayList<>();
881        Enumeration<Track> en = _trackHashTable.elements();
882        while (en.hasMoreElements()) {
883            out.add(en.nextElement());
884        }
885        return out;
886    }
887
888    /**
889     * Sorted list by track name. Returns a list of tracks of a given track
890     * type. If type is null returns all tracks for the location.
891     *
892     * @param type track type: Track.YARD, Track.SPUR, Track.INTERCHANGE,
893     *             Track.STAGING
894     * @return list of tracks ordered by name for this location
895     */
896    public List<Track> getTracksByNameList(String type) {
897        List<Track> out = new ArrayList<>();
898        for (Track track : getTracksByIdList()) {
899            boolean locAdded = false;
900            for (int j = 0; j < out.size(); j++) {
901                if (track.getName().compareToIgnoreCase(out.get(j).getName()) < 0 &&
902                        (type != null && track.getTrackType().equals(type) || type == null)) {
903                    out.add(j, track);
904                    locAdded = true;
905                    break;
906                }
907            }
908            if (!locAdded && (type != null && track.getTrackType().equals(type) || type == null)) {
909                out.add(track);
910            }
911        }
912        return out;
913    }
914
915    /**
916     * Sorted list by track moves. Returns a list of a given track type. If type
917     * is null, all tracks for the location are returned. Tracks with schedules
918     * are placed at the start of the list. Tracks that are alternates are
919     * removed.
920     *
921     * @param type track type: Track.YARD, Track.SPUR, Track.INTERCHANGE,
922     *             Track.STAGING
923     * @return list of tracks at this location ordered by moves
924     */
925    public List<Track> getTracksByMoves(String type) {
926        List<Track> moveList = new ArrayList<>();
927        for (Track track : getTracksByIdList()) {
928            boolean locAdded = false;
929            for (int j = 0; j < moveList.size(); j++) {
930                if (track.getMoves() < moveList.get(j).getMoves() &&
931                        (type != null && track.getTrackType().equals(type) || type == null)) {
932                    moveList.add(j, track);
933                    locAdded = true;
934                    break;
935                }
936            }
937            if (!locAdded && (type != null && track.getTrackType().equals(type) || type == null)) {
938                moveList.add(track);
939            }
940        }
941        // remove alternate tracks from the list
942        // bias tracks with schedules to the start of the list
943        List<Track> out = new ArrayList<>();
944        for (int i = 0; i < moveList.size(); i++) {
945            Track track = moveList.get(i);
946            if (track.isAlternate()) {
947                moveList.remove(i--);
948            } else if (!track.getScheduleId().equals(NONE)) {
949                out.add(track);
950                moveList.remove(i--);
951            }
952        }
953        for (Track track : moveList) {
954            out.add(track);
955        }
956        return out;
957    }
958
959    /**
960     * Sorted list by track blocking order. Returns a list of a given track
961     * type. If type is null, all tracks for the location are returned.
962     *
963     * @param type track type: Track.YARD, Track.SPUR, Track.INTERCHANGE,
964     *             Track.STAGING
965     * @return list of tracks at this location ordered by blocking order
966     */
967    public List<Track> getTracksByBlockingOrderList(String type) {
968        List<Track> orderList = new ArrayList<>();
969        for (Track track : getTracksByNameList(type)) {
970            for (int j = 0; j < orderList.size(); j++) {
971                if (track.getBlockingOrder() < orderList.get(j).getBlockingOrder()) {
972                    orderList.add(j, track);
973                    break;
974                }
975            }
976            if (!orderList.contains(track)) {
977                orderList.add(track);
978            }
979        }
980        return orderList;
981    }
982
983    public void resetTracksByBlockingOrder() {
984        for (Track track : getTracksList()) {
985            track.setBlockingOrder(0);
986        }
987        setDirtyAndFirePropertyChange(TRACK_BLOCKING_ORDER_CHANGED_PROPERTY, true, false);
988    }
989
990    public void resequnceTracksByBlockingOrder() {
991        int order = 1;
992        for (Track track : getTracksByBlockingOrderList(null)) {
993            track.setBlockingOrder(order++);
994        }
995        setDirtyAndFirePropertyChange(TRACK_BLOCKING_ORDER_CHANGED_PROPERTY, true, false);
996    }
997
998    public void changeTrackBlockingOrderEarlier(Track track) {
999        // if track blocking order is 0, then the blocking table has never been
1000        // initialized
1001        if (track.getBlockingOrder() != 0) {
1002            // first adjust the track being replaced
1003            Track repalceTrack = getTrackByBlockingOrder(track.getBlockingOrder() - 1);
1004            if (repalceTrack != null) {
1005                repalceTrack.setBlockingOrder(track.getBlockingOrder());
1006            }
1007            track.setBlockingOrder(track.getBlockingOrder() - 1);
1008            // move the end of order
1009            if (track.getBlockingOrder() <= 0)
1010                track.setBlockingOrder(getNumberOfTracks() + 1);
1011        }
1012        resequnceTracksByBlockingOrder();
1013    }
1014
1015    public void changeTrackBlockingOrderLater(Track track) {
1016        // if track blocking order is 0, then the blocking table has never been
1017        // initialized
1018        if (track.getBlockingOrder() != 0) {
1019            // first adjust the track being replaced
1020            Track repalceTrack = getTrackByBlockingOrder(track.getBlockingOrder() + 1);
1021            if (repalceTrack != null) {
1022                repalceTrack.setBlockingOrder(track.getBlockingOrder());
1023            }
1024            track.setBlockingOrder(track.getBlockingOrder() + 1);
1025            // move the start of order
1026            if (track.getBlockingOrder() > getNumberOfTracks())
1027                track.setBlockingOrder(0);
1028        }
1029        resequnceTracksByBlockingOrder();
1030    }
1031
1032    private Track getTrackByBlockingOrder(int order) {
1033        for (Track track : getTracksList()) {
1034            if (track.getBlockingOrder() == order)
1035                return track;
1036        }
1037        return null; // not found!
1038    }
1039
1040    public boolean isTrackAtLocation(Track track) {
1041        if (track == null) {
1042            return true;
1043        }
1044        return _trackHashTable.contains(track);
1045    }
1046
1047    /**
1048     * Reset the move count for all tracks at this location
1049     */
1050    public void resetMoves() {
1051        List<Track> tracks = getTracksList();
1052        for (Track track : tracks) {
1053            track.setMoves(0);
1054        }
1055    }
1056
1057    /**
1058     * Updates a JComboBox with all of the track locations for this location.
1059     *
1060     * @param box JComboBox to be updated.
1061     */
1062    public void updateComboBox(JComboBox<Track> box) {
1063        box.removeAllItems();
1064        box.addItem(null);
1065        List<Track> tracks = getTracksByNameList(null);
1066        for (Track track : tracks) {
1067            box.addItem(track);
1068        }
1069        OperationsPanel.padComboBox(box, InstanceManager.getDefault(LocationManager.class).getMaxTrackNameLength());
1070    }
1071
1072    /**
1073     * Updates a JComboBox with tracks that can service the rolling stock.
1074     *
1075     * @param box           JComboBox to be updated.
1076     * @param rs            Rolling Stock to be serviced
1077     * @param filter        When true, remove tracks not able to service rs.
1078     * @param isDestination When true, the tracks are destinations for the rs.
1079     */
1080    public void updateComboBox(JComboBox<Track> box, RollingStock rs, boolean filter, boolean isDestination) {
1081        updateComboBox(box);
1082        if (!filter || rs == null) {
1083            return;
1084        }
1085        List<Track> tracks = getTracksByNameList(null);
1086        for (Track track : tracks) {
1087            String status = "";
1088            if (isDestination) {
1089                status = rs.checkDestination(this, track);
1090            } else {
1091                status = rs.testLocation(this, track);
1092            }
1093            if (status.equals(Track.OKAY) && (!isDestination || !track.isStaging())) {
1094                box.setSelectedItem(track);
1095                log.debug("Available track: {} for location: {}", track.getName(), getName());
1096            } else {
1097                box.removeItem(track);
1098            }
1099        }
1100    }
1101
1102    /**
1103     * Adds a track pool for this location. A track pool is a set of tracks
1104     * where the length of the tracks is shared between all of them.
1105     *
1106     * @param name the name of the Pool to create
1107     * @return Pool
1108     */
1109    public Pool addPool(String name) {
1110        Pool pool = getPoolByName(name);
1111        if (pool == null && !name.isBlank()) {
1112            _idPoolNumber++;
1113            String id = getId() + "p" + Integer.toString(_idPoolNumber);
1114            log.debug("creating new pool ({}) id: {}", name, id);
1115            pool = new Pool(id, name);
1116            register(pool);
1117        }
1118        return pool;
1119    }
1120
1121    public void removePool(Pool pool) {
1122        if (pool != null) {
1123            _poolHashTable.remove(pool.getId());
1124            setDirtyAndFirePropertyChange(POOL_LENGTH_CHANGED_PROPERTY, _poolHashTable.size() + 1,
1125                    _poolHashTable.size());
1126        }
1127    }
1128
1129    public Pool getPoolByName(String name) {
1130        Pool pool;
1131        Enumeration<Pool> en = _poolHashTable.elements();
1132        while (en.hasMoreElements()) {
1133            pool = en.nextElement();
1134            if (pool.getName().equals(name)) {
1135                return pool;
1136            }
1137        }
1138        return null;
1139    }
1140
1141    public void register(Pool pool) {
1142        int old = _poolHashTable.size();
1143        _poolHashTable.put(pool.getId(), pool);
1144        // find last id created
1145        String[] getId = pool.getId().split("p");
1146        int id = Integer.parseInt(getId[1]);
1147        if (id > _idPoolNumber) {
1148            _idPoolNumber = id;
1149        }
1150        setDirtyAndFirePropertyChange(POOL_LENGTH_CHANGED_PROPERTY, old, _poolHashTable.size());
1151    }
1152
1153    public void updatePoolComboBox(JComboBox<Pool> box) {
1154        box.removeAllItems();
1155        box.addItem(null);
1156        for (Pool pool : getPoolsByNameList()) {
1157            box.addItem(pool);
1158        }
1159    }
1160
1161    /**
1162     * Gets a list of Pools for this location.
1163     *
1164     * @return A list of Pools
1165     */
1166    public List<Pool> getPoolsByNameList() {
1167        List<Pool> pools = new ArrayList<>();
1168        Enumeration<Pool> en = _poolHashTable.elements();
1169        while (en.hasMoreElements()) {
1170            pools.add(en.nextElement());
1171        }
1172        return pools;
1173    }
1174
1175    /**
1176     * True if this location has a track with pick up or set out restrictions.
1177     * 
1178     * @return True if there are restrictions at this location.
1179     */
1180    public boolean hasServiceRestrictions() {
1181        Track track;
1182        Enumeration<Track> en = _trackHashTable.elements();
1183        while (en.hasMoreElements()) {
1184            track = en.nextElement();
1185            if (!track.getDropOption().equals(Track.ANY) || !track.getPickupOption().equals(Track.ANY)) {
1186                return true;
1187            }
1188        }
1189        return false;
1190    }
1191
1192    /**
1193     * Used to determine if there are Pools at this location.
1194     *
1195     * @return True if there are Pools at this location
1196     */
1197    public boolean hasPools() {
1198        return _poolHashTable.size() > 0;
1199    }
1200
1201    /**
1202     * Used to determine if there are any planned pickups at this location.
1203     *
1204     * @return True if there are planned pickups
1205     */
1206    public boolean hasPlannedPickups() {
1207        List<Track> tracks = getTracksList();
1208        for (Track track : tracks) {
1209            if (track.getIgnoreUsedLengthPercentage() > Track.IGNORE_0) {
1210                return true;
1211            }
1212        }
1213        return false;
1214    }
1215
1216    /**
1217     * Used to determine if there are any load restrictions at this location.
1218     *
1219     * @return True if there are load restrictions
1220     */
1221    public boolean hasLoadRestrictions() {
1222        List<Track> tracks = getTracksList();
1223        for (Track track : tracks) {
1224            if (!track.getLoadOption().equals(Track.ALL_LOADS)) {
1225                return true;
1226            }
1227        }
1228        return false;
1229    }
1230
1231    /**
1232     * Used to determine if there are any load ship restrictions at this
1233     * location.
1234     *
1235     * @return True if there are load ship restrictions
1236     */
1237    public boolean hasShipLoadRestrictions() {
1238        List<Track> tracks = getTracksList();
1239        for (Track track : tracks) {
1240            if (!track.getShipLoadOption().equals(Track.ALL_LOADS)) {
1241                return true;
1242            }
1243        }
1244        return false;
1245    }
1246
1247    /**
1248     * Used to determine if there are any road restrictions at this location.
1249     *
1250     * @return True if there are road restrictions
1251     */
1252    public boolean hasRoadRestrictions() {
1253        List<Track> tracks = getTracksList();
1254        for (Track track : tracks) {
1255            if (!track.getRoadOption().equals(Track.ALL_ROADS)) {
1256                return true;
1257            }
1258        }
1259        return false;
1260    }
1261
1262    /**
1263     * Used to determine if there are any track destination restrictions at this
1264     * location.
1265     *
1266     * @return True if there are destination restrictions
1267     */
1268    public boolean hasDestinationRestrictions() {
1269        List<Track> tracks = getTracksList();
1270        for (Track track : tracks) {
1271            if (!track.getDestinationOption().equals(Track.ALL_DESTINATIONS)) {
1272                return true;
1273            }
1274        }
1275        return false;
1276    }
1277
1278    public boolean hasAlternateTracks() {
1279        for (Track track : getTracksList()) {
1280            if (track.getAlternateTrack() != null) {
1281                return true;
1282            }
1283        }
1284        return false;
1285    }
1286
1287    public boolean hasOrderRestrictions() {
1288        for (Track track : getTracksList()) {
1289            if (!track.getServiceOrder().equals(Track.NORMAL)) {
1290                return true;
1291            }
1292        }
1293        return false;
1294    }
1295
1296    public boolean hasSchedules() {
1297        for (Track track : getTracksList()) {
1298            if (track.isSpur() && track.getSchedule() != null) {
1299                return true;
1300            }
1301        }
1302        return false;
1303    }
1304
1305    public boolean hasWork() {
1306        return (getDropRS() != 0 || getPickupRS() != 0);
1307    }
1308
1309    public boolean hasDisableLoadChange() {
1310        for (Track track : getTracksList()) {
1311            if (track.isSpur() && track.isDisableLoadChangeEnabled()) {
1312                return true;
1313            }
1314        }
1315        return false;
1316    }
1317
1318    public boolean hasQuickService() {
1319        for (Track track : getTracksList()) {
1320            if (track.isQuickServiceEnabled()) {
1321                return true;
1322            }
1323        }
1324        return Setup.isBuildOnTime();
1325    }
1326
1327    public boolean hasTracksWithRestrictedTrainDirections() {
1328        int trainDirections = getTrainDirections() & Setup.getTrainDirection();
1329        for (Track track : getTracksList()) {
1330            if (trainDirections != (track.getTrainDirections() & trainDirections)) {
1331                return true;
1332            }
1333        }
1334        return false;
1335    }
1336
1337    public boolean hasTrackMessages() {
1338        for (Track track : getTracksList()) {
1339            if (track.hasMessages()) {
1340                return true;
1341            }
1342        }
1343        return false;
1344    }
1345
1346    public boolean hasTrackPriortyChanges() {
1347        for (Track track : getTracksList()) {
1348            if (!track.getTrackPriority().equals(Track.PRIORITY_NORMAL)) {
1349                return true;
1350            }
1351        }
1352        return false;
1353    }
1354
1355    public boolean hasReporters() {
1356        for (Track track : getTracksList()) {
1357            if (track.getReporter() != null) {
1358                return true;
1359            }
1360        }
1361        return false;
1362    }
1363
1364    /*
1365     * set the jmri.Reporter object associated with this location.
1366     * 
1367     * @param reader jmri.Reporter object.
1368     */
1369    public void setReporter(Reporter r) {
1370        Reporter old = _reader;
1371        _reader = r;
1372        if (old != r) {
1373            setDirtyAndFirePropertyChange(LOCATION_REPORTER_CHANGED_PROPERTY, old, r);
1374        }
1375    }
1376
1377    /*
1378     * get the jmri.Reporter object associated with this location.
1379     * 
1380     * @return jmri.Reporter object.
1381     */
1382    public Reporter getReporter() {
1383        return _reader;
1384    }
1385
1386    public String getReporterName() {
1387        if (getReporter() != null) {
1388            return getReporter().getDisplayName();
1389        }
1390        return "";
1391    }
1392
1393    public void dispose() {
1394        List<Track> tracks = getTracksList();
1395        for (Track track : tracks) {
1396            deleteTrack(track);
1397        }
1398        InstanceManager.getDefault(CarTypes.class).removePropertyChangeListener(this);
1399        InstanceManager.getDefault(CarRoads.class).removePropertyChangeListener(this);
1400        InstanceManager.getDefault(EngineTypes.class).removePropertyChangeListener(this);
1401        // Change name in case object is still in use, for example Schedules
1402        setName(Bundle.getMessage("NotValid", getName()));
1403        setDirtyAndFirePropertyChange(DISPOSE_CHANGED_PROPERTY, null, DISPOSE_CHANGED_PROPERTY);
1404    }
1405
1406    private void addPropertyChangeListeners() {
1407        InstanceManager.getDefault(CarTypes.class).addPropertyChangeListener(this);
1408        InstanceManager.getDefault(CarRoads.class).addPropertyChangeListener(this);
1409        InstanceManager.getDefault(EngineTypes.class).addPropertyChangeListener(this);
1410    }
1411
1412    /**
1413     * Construct this Entry from XML. This member has to remain synchronized
1414     * with the detailed DTD in operations-locations.dtd
1415     *
1416     * @param e Consist XML element
1417     */
1418    public Location(Element e) {
1419        Attribute a;
1420        if ((a = e.getAttribute(Xml.ID)) != null) {
1421            _id = a.getValue();
1422        } else {
1423            log.warn("no id attribute in location element when reading operations");
1424        }
1425        if ((a = e.getAttribute(Xml.NAME)) != null) {
1426            _name = a.getValue();
1427        }
1428        if ((a = e.getAttribute(Xml.DIVISION_ID)) != null) {
1429            _division = InstanceManager.getDefault(DivisionManager.class).getDivisionById(a.getValue());
1430        }
1431        // TODO remove the following 3 lines in 2023
1432        if ((a = e.getAttribute(Xml.DIVISION_ID_ERROR)) != null) {
1433            _division = InstanceManager.getDefault(DivisionManager.class).getDivisionById(a.getValue());
1434        }
1435        if ((a = e.getAttribute(Xml.DIR)) != null) {
1436            try {
1437                _trainDir = Integer.parseInt(a.getValue());
1438            } catch (NumberFormatException nfe) {
1439                log.error("Train directions isn't a vaild number for location {}", getName());
1440            }
1441        }
1442        if ((a = e.getAttribute(Xml.SWITCH_LIST)) != null) {
1443            _switchList = (a.getValue().equals(Xml.TRUE));
1444        }
1445        if ((a = e.getAttribute(Xml.SWITCH_LIST_STATE)) != null) {
1446            try {
1447                _switchListState = Integer.parseInt(a.getValue());
1448            } catch (NumberFormatException nfe) {
1449                log.error("Switch list state isn't a vaild number for location {}", getName());
1450            }
1451            if (getSwitchListState() == SW_PRINTED) {
1452                setStatus(PRINTED);
1453            }
1454        }
1455        if ((a = e.getAttribute(Xml.PRINTER_NAME)) != null) {
1456            _defaultPrinter = a.getValue();
1457        }
1458        // load train icon coordinates
1459        Attribute x;
1460        Attribute y;
1461        try {
1462            if ((x = e.getAttribute(Xml.EAST_TRAIN_ICON_X)) != null &&
1463                    (y = e.getAttribute(Xml.EAST_TRAIN_ICON_Y)) != null) {
1464                setTrainIconEast(new Point(Integer.parseInt(x.getValue()), Integer.parseInt(y.getValue())));
1465            }
1466            if ((x = e.getAttribute(Xml.WEST_TRAIN_ICON_X)) != null &&
1467                    (y = e.getAttribute(Xml.WEST_TRAIN_ICON_Y)) != null) {
1468                setTrainIconWest(new Point(Integer.parseInt(x.getValue()), Integer.parseInt(y.getValue())));
1469            }
1470            if ((x = e.getAttribute(Xml.NORTH_TRAIN_ICON_X)) != null &&
1471                    (y = e.getAttribute(Xml.NORTH_TRAIN_ICON_Y)) != null) {
1472                setTrainIconNorth(new Point(Integer.parseInt(x.getValue()), Integer.parseInt(y.getValue())));
1473            }
1474            if ((x = e.getAttribute(Xml.SOUTH_TRAIN_ICON_X)) != null &&
1475                    (y = e.getAttribute(Xml.SOUTH_TRAIN_ICON_Y)) != null) {
1476                setTrainIconSouth(new Point(Integer.parseInt(x.getValue()), Integer.parseInt(y.getValue())));
1477            }
1478
1479            if ((x = e.getAttribute(Xml.TRAIN_ICON_RANGE_X)) != null) {
1480                setTrainIconRangeX(Integer.parseInt(x.getValue()));
1481            }
1482            if ((y = e.getAttribute(Xml.TRAIN_ICON_RANGE_Y)) != null) {
1483                setTrainIconRangeY(Integer.parseInt(y.getValue()));
1484            }
1485
1486        } catch (NumberFormatException nfe) {
1487            log.error("Train icon coordinates aren't vaild for location {}", getName());
1488        }
1489
1490        if ((a = e.getAttribute(Xml.COMMENT)) != null) {
1491            _comment = a.getValue();
1492        }
1493
1494        if ((a = e.getAttribute(Xml.SWITCH_LIST_COMMENT)) != null) {
1495            _switchListComment = a.getValue();
1496        }
1497        if ((a = e.getAttribute(Xml.PHYSICAL_LOCATION)) != null) {
1498            _physicalLocation = PhysicalLocation.parse(a.getValue());
1499        }
1500        // new way of reading car types using elements added in 3.3.1
1501        if (e.getChild(Xml.TYPES) != null) {
1502            List<Element> carTypes = e.getChild(Xml.TYPES).getChildren(Xml.CAR_TYPE);
1503            String[] types = new String[carTypes.size()];
1504            for (int i = 0; i < carTypes.size(); i++) {
1505                Element type = carTypes.get(i);
1506                if ((a = type.getAttribute(Xml.NAME)) != null) {
1507                    types[i] = a.getValue();
1508                }
1509            }
1510            setTypeNames(types);
1511            List<Element> locoTypes = e.getChild(Xml.TYPES).getChildren(Xml.LOCO_TYPE);
1512            types = new String[locoTypes.size()];
1513            for (int i = 0; i < locoTypes.size(); i++) {
1514                Element type = locoTypes.get(i);
1515                if ((a = type.getAttribute(Xml.NAME)) != null) {
1516                    types[i] = a.getValue();
1517                }
1518            }
1519            setTypeNames(types);
1520        } // old way of reading car types up to version 2.99.6
1521        else if ((a = e.getAttribute(Xml.CAR_TYPES)) != null) {
1522            String names = a.getValue();
1523            String[] Types = names.split("%%"); // NOI18N
1524            setTypeNames(Types);
1525        }
1526        // early version of operations called tracks "secondary"
1527        if (e.getChildren(Xml.SECONDARY) != null) {
1528            List<Element> eTracks = e.getChildren(Xml.SECONDARY);
1529            for (Element eTrack : eTracks) {
1530                register(new Track(eTrack, this));
1531            }
1532        }
1533        if (e.getChildren(Xml.TRACK) != null) {
1534            List<Element> eTracks = e.getChildren(Xml.TRACK);
1535            log.debug("location ({}) has {} tracks", getName(), eTracks.size());
1536            for (Element eTrack : eTracks) {
1537                register(new Track(eTrack, this));
1538            }
1539        }
1540        if (e.getAttribute(Xml.READER) != null) {
1541            // @SuppressWarnings("unchecked")
1542            try {
1543                Reporter r = jmri.InstanceManager.getDefault(jmri.ReporterManager.class)
1544                        .provideReporter(e.getAttribute(Xml.READER).getValue());
1545                _reader = r;
1546            } catch (IllegalArgumentException ex) {
1547                log.warn("Not able to find reader: {} for location ({})", e.getAttribute(Xml.READER).getValue(),
1548                        getName());
1549            }
1550        }
1551        addPropertyChangeListeners();
1552    }
1553
1554    /**
1555     * Create an XML element to represent this Entry. This member has to remain
1556     * synchronized with the detailed DTD in operations-locations.dtd.
1557     *
1558     * @return Contents in a JDOM Element
1559     */
1560    public Element store() {
1561        Element e = new Element(Xml.LOCATION);
1562        e.setAttribute(Xml.ID, getId());
1563        e.setAttribute(Xml.NAME, getName());
1564        if (!getDivisionId().equals(NONE)) {
1565            e.setAttribute(Xml.DIVISION_ID, getDivisionId());
1566        }
1567        // backwards compatibility starting 2/6/2021, remove after 2023
1568        e.setAttribute(Xml.OPS, isStaging() ? STAGING : NORMAL);
1569        e.setAttribute(Xml.DIR, Integer.toString(getTrainDirections()));
1570        e.setAttribute(Xml.SWITCH_LIST, isSwitchListEnabled() ? Xml.TRUE : Xml.FALSE);
1571        if (!Setup.isSwitchListRealTime()) {
1572            e.setAttribute(Xml.SWITCH_LIST_STATE, Integer.toString(getSwitchListState()));
1573        }
1574        if (!getDefaultPrinterName().equals(NONE)) {
1575            e.setAttribute(Xml.PRINTER_NAME, getDefaultPrinterName());
1576        }
1577        if (!getTrainIconEast().equals(new Point())) {
1578            e.setAttribute(Xml.EAST_TRAIN_ICON_X, Integer.toString(getTrainIconEast().x));
1579            e.setAttribute(Xml.EAST_TRAIN_ICON_Y, Integer.toString(getTrainIconEast().y));
1580        }
1581        if (!getTrainIconWest().equals(new Point())) {
1582            e.setAttribute(Xml.WEST_TRAIN_ICON_X, Integer.toString(getTrainIconWest().x));
1583            e.setAttribute(Xml.WEST_TRAIN_ICON_Y, Integer.toString(getTrainIconWest().y));
1584        }
1585        if (!getTrainIconNorth().equals(new Point())) {
1586            e.setAttribute(Xml.NORTH_TRAIN_ICON_X, Integer.toString(getTrainIconNorth().x));
1587            e.setAttribute(Xml.NORTH_TRAIN_ICON_Y, Integer.toString(getTrainIconNorth().y));
1588        }
1589        if (!getTrainIconSouth().equals(new Point())) {
1590            e.setAttribute(Xml.SOUTH_TRAIN_ICON_X, Integer.toString(getTrainIconSouth().x));
1591            e.setAttribute(Xml.SOUTH_TRAIN_ICON_Y, Integer.toString(getTrainIconSouth().y));
1592        }
1593        if (getTrainIconRangeX() != RANGE_DEFAULT) {
1594            e.setAttribute(Xml.TRAIN_ICON_RANGE_X, Integer.toString(getTrainIconRangeX()));
1595        }
1596        if (getTrainIconRangeY() != RANGE_DEFAULT) {
1597            e.setAttribute(Xml.TRAIN_ICON_RANGE_Y, Integer.toString(getTrainIconRangeY()));
1598        }
1599        if (_reader != null) {
1600            e.setAttribute(Xml.READER, _reader.getDisplayName());
1601        }
1602        // build list of rolling stock types for this location
1603        String[] types = getTypeNames();
1604        // new way of saving car types
1605        Element eTypes = new Element(Xml.TYPES);
1606        for (String type : types) {
1607            // don't save types that have been deleted by user
1608            if (InstanceManager.getDefault(EngineTypes.class).containsName(type)) {
1609                Element eType = new Element(Xml.LOCO_TYPE);
1610                eType.setAttribute(Xml.NAME, type);
1611                eTypes.addContent(eType);
1612            } else if (InstanceManager.getDefault(CarTypes.class).containsName(type)) {
1613                Element eType = new Element(Xml.CAR_TYPE);
1614                eType.setAttribute(Xml.NAME, type);
1615                eTypes.addContent(eType);
1616            }
1617        }
1618        e.addContent(eTypes);
1619
1620        // save physical location if not default
1621        if (getPhysicalLocation() != null && !getPhysicalLocation().equals(PhysicalLocation.Origin)) {
1622            e.setAttribute(Xml.PHYSICAL_LOCATION, getPhysicalLocation().toString());
1623        }
1624
1625        e.setAttribute(Xml.COMMENT, getCommentWithColor());
1626        e.setAttribute(Xml.SWITCH_LIST_COMMENT, getSwitchListCommentWithColor());
1627
1628        List<Track> tracks = getTracksByIdList();
1629        for (Track track : tracks) {
1630            e.addContent(track.store());
1631        }
1632
1633        return e;
1634    }
1635
1636    private void replaceType(String oldType, String newType) {
1637        if (acceptsTypeName(oldType)) {
1638            if (newType != null) {
1639                addTypeName(newType);
1640            }
1641            // now adjust tracks
1642            List<Track> tracks = getTracksList();
1643            for (Track track : tracks) {
1644                if (track.isTypeNameAccepted(oldType)) {
1645                    track.deleteTypeName(oldType);
1646                    if (newType != null) {
1647                        track.addTypeName(newType);
1648                    }
1649                }
1650                // adjust custom loads
1651                String[] loadNames = track.getLoadNames();
1652                for (String load : loadNames) {
1653                    String[] splitLoad = load.split(CarLoad.SPLIT_CHAR);
1654                    if (splitLoad.length > 1) {
1655                        if (splitLoad[0].equals(oldType)) {
1656                            track.deleteLoadName(load);
1657                            if (newType != null) {
1658                                load = newType + CarLoad.SPLIT_CHAR + splitLoad[1];
1659                                track.addLoadName(load);
1660                            }
1661                        }
1662                    }
1663                }
1664                loadNames = track.getShipLoadNames();
1665                for (String load : loadNames) {
1666                    String[] splitLoad = load.split(CarLoad.SPLIT_CHAR);
1667                    if (splitLoad.length > 1) {
1668                        if (splitLoad[0].equals(oldType)) {
1669                            track.deleteShipLoadName(load);
1670                            if (newType != null) {
1671                                load = newType + CarLoad.SPLIT_CHAR + splitLoad[1];
1672                                track.addShipLoadName(load);
1673                            }
1674                        }
1675                    }
1676                }
1677            }
1678            deleteTypeName(oldType);
1679        }
1680    }
1681
1682    private void replaceRoad(String oldRoad, String newRoad) {
1683        // now adjust any track locations
1684        List<Track> tracks = getTracksList();
1685        for (Track track : tracks) {
1686            if (track.containsRoadName(oldRoad)) {
1687                track.deleteRoadName(oldRoad);
1688                if (newRoad != null) {
1689                    track.addRoadName(newRoad);
1690                }
1691            }
1692        }
1693    }
1694
1695    @Override
1696    public void propertyChange(java.beans.PropertyChangeEvent e) {
1697        if (Control.SHOW_PROPERTY) {
1698            log.debug("Property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e.getOldValue(),
1699                    e.getNewValue());
1700        }
1701        // update length of tracks at this location if track length changes
1702        if (e.getPropertyName().equals(Track.LENGTH_CHANGED_PROPERTY)) {
1703            setLength(getLength() - (int) e.getOldValue() + (int) e.getNewValue());
1704        }
1705        // if a track type change, must update all tables
1706        if (e.getPropertyName().equals(Track.TRACK_TYPE_CHANGED_PROPERTY)) {
1707            setDirtyAndFirePropertyChange(TRACK_LISTLENGTH_CHANGED_PROPERTY, false, true);
1708        }
1709        if (e.getPropertyName().equals(CarTypes.CARTYPES_NAME_CHANGED_PROPERTY) ||
1710                e.getPropertyName().equals(CarTypes.CARTYPES_CHANGED_PROPERTY) ||
1711                e.getPropertyName().equals(EngineTypes.ENGINETYPES_NAME_CHANGED_PROPERTY)) {
1712            replaceType((String) e.getOldValue(), (String) e.getNewValue());
1713        }
1714        if (e.getPropertyName().equals(CarRoads.CARROADS_NAME_CHANGED_PROPERTY)) {
1715            replaceRoad((String) e.getOldValue(), (String) e.getNewValue());
1716        }
1717    }
1718
1719    protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
1720        InstanceManager.getDefault(LocationManagerXml.class).setDirty(true);
1721        firePropertyChange(p, old, n);
1722    }
1723
1724    private static final Logger log = LoggerFactory.getLogger(Location.class);
1725
1726}