001package jmri.jmrit.operations.locations;
002
003import java.awt.Point;
004import java.beans.PropertyChangeListener;
005import java.text.MessageFormat;
006import java.util.*;
007
008import javax.swing.JComboBox;
009
010import org.jdom2.Attribute;
011import org.jdom2.Element;
012import org.slf4j.Logger;
013import org.slf4j.LoggerFactory;
014
015import jmri.InstanceManager;
016import jmri.Reporter;
017import jmri.beans.Identifiable;
018import jmri.beans.PropertyChangeSupport;
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.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    /**
157     * Makes a copy of this location.
158     *
159     * @param newLocation the location to copy to
160     */
161    public void copyLocation(Location newLocation) {
162        newLocation.setComment(getCommentWithColor());
163        newLocation.setDefaultPrinterName(getDefaultPrinterName());
164        newLocation.setSwitchListComment(getSwitchListCommentWithColor());
165        newLocation.setSwitchListEnabled(isSwitchListEnabled());
166        newLocation.setTrainDirections(getTrainDirections());
167        // TODO should we set the train icon coordinates?
168        // rolling stock serviced by this location
169        for (String type : newLocation.getTypeNames()) {
170            if (acceptsTypeName(type)) {
171                continue;
172            } else {
173                newLocation.deleteTypeName(type);
174            }
175        }
176        copyTracksLocation(newLocation);
177    }
178
179    /**
180     * Copies all of the tracks at this location. If there's a track already at
181     * the copy to location with the same name, the track is skipped.
182     *
183     * @param location the location to copy the tracks to.
184     */
185    public void copyTracksLocation(Location location) {
186        for (Track track : getTracksList()) {
187            if (location.getTrackByName(track.getName(), null) != null) {
188                continue;
189            }
190            track.copyTrack(track.getName(), location);
191        }
192    }
193
194    public PhysicalLocation getPhysicalLocation() {
195        return (_physicalLocation);
196    }
197
198    public void setPhysicalLocation(PhysicalLocation l) {
199        _physicalLocation = l;
200        InstanceManager.getDefault(LocationManagerXml.class).setDirty(true);
201    }
202
203    /**
204     * Set total length of all tracks for this location
205     * 
206     * @param length The integer sum of all tracks at this location.
207     */
208    public void setLength(int length) {
209        int old = _length;
210        _length = length;
211        if (old != length) {
212            setDirtyAndFirePropertyChange(LENGTH_CHANGED_PROPERTY, Integer.toString(old), Integer.toString(length));
213        }
214    }
215
216    /**
217     * @return total length of all tracks for this location
218     */
219    public int getLength() {
220        return _length;
221    }
222
223    public void setUsedLength(int length) {
224        int old = _usedLength;
225        _usedLength = length;
226        if (old != length) {
227            setDirtyAndFirePropertyChange(USEDLENGTH_CHANGED_PROPERTY, Integer.toString(old), Integer.toString(length));
228        }
229    }
230
231    /**
232     * @return The length of the track that is occupied by cars and engines
233     */
234    public int getUsedLength() {
235        return _usedLength;
236    }
237
238    /**
239     * Used to determine if location is setup for staging
240     *
241     * @return true if location is setup as staging
242     */
243    public boolean isStaging() {
244        return hasTrackType(Track.STAGING);
245    }
246
247    /**
248     * @return True if location has spurs
249     */
250    public boolean hasSpurs() {
251        return hasTrackType(Track.SPUR);
252    }
253
254    /**
255     * @return True if location has classification/interchange tracks
256     */
257    public boolean hasInterchanges() {
258        return hasTrackType(Track.INTERCHANGE);
259    }
260
261    /**
262     * @return True if location has yard tracks
263     */
264    public boolean hasYards() {
265        return hasTrackType(Track.YARD);
266    }
267
268    /**
269     * @param trackType The track type to check.
270     * @return True if location has the track type specified Track.INTERCHANGE
271     *         Track.YARD Track.SPUR Track.Staging
272     */
273    public boolean hasTrackType(String trackType) {
274        Track track;
275        Enumeration<Track> en = _trackHashTable.elements();
276        while (en.hasMoreElements()) {
277            track = en.nextElement();
278            if (track.getTrackType().equals(trackType)) {
279                return true;
280            }
281        }
282        return false;
283    }
284
285    /**
286     * Change all tracks at this location to type
287     * 
288     * @param type Track.INTERCHANGE Track.YARD Track.SPUR Track.Staging
289     */
290    public void changeTrackType(String type) {
291        List<Track> tracks = getTracksByNameList(null);
292        for (Track track : tracks) {
293            track.setTrackType(type);
294        }
295    }
296
297    public int getNumberOfTracks() {
298        return _trackHashTable.size();
299    }
300
301    /**
302     * Sets the train directions that this location can service. EAST means that
303     * an Eastbound train can service the location.
304     *
305     * @param direction Any combination of EAST WEST NORTH SOUTH
306     */
307    public void setTrainDirections(int direction) {
308        int old = _trainDir;
309        _trainDir = direction;
310        if (old != direction) {
311            setDirtyAndFirePropertyChange(TRAIN_DIRECTION_CHANGED_PROPERTY, Integer.toString(old),
312                    Integer.toString(direction));
313        }
314    }
315
316    /**
317     * Gets the train directions that this location can service. EAST means that
318     * an Eastbound train can service the location.
319     *
320     * @return Any combination of EAST WEST NORTH SOUTH
321     */
322    public int getTrainDirections() {
323        return _trainDir;
324    }
325
326    /**
327     * Sets the quantity of rolling stock for this location
328     * 
329     * @param number An integer representing the quantity of rolling stock at
330     *               this location.
331     */
332    public void setNumberRS(int number) {
333        int old = _numberRS;
334        _numberRS = number;
335        if (old != number) {
336            setDirtyAndFirePropertyChange("locationNumberRS", Integer.toString(old), Integer.toString(number)); // NOI18N
337        }
338    }
339
340    /**
341     * Gets the number of cars and engines at this location
342     *
343     * @return number of cars at this location
344     */
345    public int getNumberRS() {
346        return _numberRS;
347    }
348
349    /**
350     * Sets the number of cars at this location
351     */
352    private void setNumberCars(int number) {
353        int old = _numberCars;
354        _numberCars = number;
355        if (old != number) {
356            setDirtyAndFirePropertyChange("locationNumberCars", Integer.toString(old), // NOI18N
357                    Integer.toString(number)); // NOI18N
358        }
359    }
360
361    /**
362     * @return The number of cars at this location
363     */
364    public int getNumberCars() {
365        return _numberCars;
366    }
367
368    /**
369     * Sets the number of engines at this location
370     */
371    private void setNumberEngines(int number) {
372        int old = _numberEngines;
373        _numberEngines = number;
374        if (old != number) {
375            setDirtyAndFirePropertyChange("locationNumberEngines", Integer.toString(old), // NOI18N
376                    Integer.toString(number)); // NOI18N
377        }
378    }
379
380    /**
381     * @return The number of engines at this location
382     */
383    public int getNumberEngines() {
384        return _numberEngines;
385    }
386
387    /**
388     * When true, a switchlist is desired for this location. Used for preview
389     * and printing a manifest for a single location
390     * 
391     * @param switchList When true, switch lists are enabled for this location.
392     */
393    public void setSwitchListEnabled(boolean switchList) {
394        boolean old = _switchList;
395        _switchList = switchList;
396        if (old != switchList) {
397            setDirtyAndFirePropertyChange(SWITCHLIST_CHANGED_PROPERTY, old ? "true" : "false", // NOI18N
398                    switchList ? "true" : "false"); // NOI18N
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.toString(), point.toString()); // 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.toString(), point.toString()); // 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.toString(), point.toString()); // 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.toString(), point.toString()); // 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", Integer.toString(old), Integer.toString(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", Integer.toString(old), Integer.toString(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", Integer.toString(old), Integer.toString(_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", Integer.toString(old), Integer.toString(_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", Integer.toString(old), Integer.toString(_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", Integer.toString(old), Integer.toString(_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.getTextColorString(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.getTextColorString(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        Integer old = Integer.valueOf(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, Integer.valueOf(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            Integer old = Integer.valueOf(getNumberOfTracks());
813            _trackHashTable.remove(track.getId());
814            setDirtyAndFirePropertyChange(TRACK_LISTLENGTH_CHANGED_PROPERTY, old,
815                    Integer.valueOf(getNumberOfTracks()));
816        }
817    }
818
819    /**
820     * Get track at this location by name and type. Track type can be null.
821     *
822     * @param name track's name
823     * @param type track type
824     * @return track at location
825     */
826    public Track getTrackByName(String name, String type) {
827        Track track;
828        Enumeration<Track> en = _trackHashTable.elements();
829        while (en.hasMoreElements()) {
830            track = en.nextElement();
831            if (type == null) {
832                if (track.getName().equals(name)) {
833                    return track;
834                }
835            } else if (track.getName().equals(name) && track.getTrackType().equals(type)) {
836                return track;
837            }
838        }
839        return null;
840    }
841
842    public Track getTrackById(String id) {
843        return _trackHashTable.get(id);
844    }
845
846    /**
847     * Gets a list of track ids ordered by id for this location.
848     *
849     * @return list of track ids for this location
850     */
851    public List<String> getTrackIdsByIdList() {
852        List<String> out = new ArrayList<>();
853        Enumeration<String> en = _trackHashTable.keys();
854        while (en.hasMoreElements()) {
855            out.add(en.nextElement());
856        }
857        Collections.sort(out);
858        return out;
859    }
860
861    /**
862     * Gets a sorted by id list of tracks for this location.
863     *
864     * @return Sorted list of tracks by id for this location.
865     */
866    public List<Track> getTracksByIdList() {
867        List<Track> out = new ArrayList<>();
868        List<String> trackIds = getTrackIdsByIdList();
869        for (String id : trackIds) {
870            out.add(getTrackById(id));
871        }
872        return out;
873    }
874
875    /**
876     * Gets a unsorted list of the tracks at this location.
877     *
878     * @return tracks at this location.
879     */
880    public List<Track> getTracksList() {
881        List<Track> out = new ArrayList<>();
882        Enumeration<Track> en = _trackHashTable.elements();
883        while (en.hasMoreElements()) {
884            out.add(en.nextElement());
885        }
886        return out;
887    }
888
889    /**
890     * Sorted list by track name. Returns a list of tracks of a given track
891     * type. If type is null returns all tracks for the location.
892     *
893     * @param type track type: Track.YARD, Track.SPUR, Track.INTERCHANGE,
894     *             Track.STAGING
895     * @return list of tracks ordered by name for this location
896     */
897    public List<Track> getTracksByNameList(String type) {
898        List<Track> out = new ArrayList<>();
899        for (Track track : getTracksByIdList()) {
900            boolean locAdded = false;
901            for (int j = 0; j < out.size(); j++) {
902                if (track.getName().compareToIgnoreCase(out.get(j).getName()) < 0 &&
903                        (type != null && track.getTrackType().equals(type) || type == null)) {
904                    out.add(j, track);
905                    locAdded = true;
906                    break;
907                }
908            }
909            if (!locAdded && (type != null && track.getTrackType().equals(type) || type == null)) {
910                out.add(track);
911            }
912        }
913        return out;
914    }
915
916    /**
917     * Sorted list by track moves. Returns a list of a given track type. If type
918     * is null, all tracks for the location are returned. Tracks with schedules
919     * are placed at the start of the list. Tracks that are alternates are
920     * removed.
921     *
922     * @param type track type: Track.YARD, Track.SPUR, Track.INTERCHANGE,
923     *             Track.STAGING
924     * @return list of tracks at this location ordered by moves
925     */
926    public List<Track> getTracksByMoves(String type) {
927        List<Track> moveList = new ArrayList<>();
928        for (Track track : getTracksByIdList()) {
929            boolean locAdded = false;
930            for (int j = 0; j < moveList.size(); j++) {
931                if (track.getMoves() < moveList.get(j).getMoves() &&
932                        (type != null && track.getTrackType().equals(type) || type == null)) {
933                    moveList.add(j, track);
934                    locAdded = true;
935                    break;
936                }
937            }
938            if (!locAdded && (type != null && track.getTrackType().equals(type) || type == null)) {
939                moveList.add(track);
940            }
941        }
942        // remove alternate tracks from the list
943        // bias tracks with schedules to the start of the list
944        List<Track> out = new ArrayList<>();
945        for (int i = 0; i < moveList.size(); i++) {
946            Track track = moveList.get(i);
947            if (track.isAlternate()) {
948                moveList.remove(i--);
949            } else if (!track.getScheduleId().equals(NONE)) {
950                out.add(track);
951                moveList.remove(i--);
952            }
953        }
954        for (Track track : moveList) {
955            out.add(track);
956        }
957        return out;
958    }
959
960    /**
961     * Sorted list by track blocking order. Returns a list of a given track
962     * type. If type is null, all tracks for the location are returned.
963     *
964     * @param type track type: Track.YARD, Track.SPUR, Track.INTERCHANGE,
965     *             Track.STAGING
966     * @return list of tracks at this location ordered by blocking order
967     */
968    public List<Track> getTracksByBlockingOrderList(String type) {
969        List<Track> orderList = new ArrayList<>();
970        for (Track track : getTracksByNameList(type)) {
971            for (int j = 0; j < orderList.size(); j++) {
972                if (track.getBlockingOrder() < orderList.get(j).getBlockingOrder()) {
973                    orderList.add(j, track);
974                    break;
975                }
976            }
977            if (!orderList.contains(track)) {
978                orderList.add(track);
979            }
980        }
981        return orderList;
982    }
983
984    public void resetTracksByBlockingOrder() {
985        for (Track track : getTracksList()) {
986            track.setBlockingOrder(0);
987        }
988        setDirtyAndFirePropertyChange(TRACK_BLOCKING_ORDER_CHANGED_PROPERTY, true, false);
989    }
990
991    public void resequnceTracksByBlockingOrder() {
992        int order = 1;
993        for (Track track : getTracksByBlockingOrderList(null)) {
994            track.setBlockingOrder(order++);
995        }
996        setDirtyAndFirePropertyChange(TRACK_BLOCKING_ORDER_CHANGED_PROPERTY, true, false);
997    }
998
999    public void changeTrackBlockingOrderEarlier(Track track) {
1000        // if track blocking order is 0, then the blocking table has never been
1001        // initialized
1002        if (track.getBlockingOrder() != 0) {
1003            // first adjust the track being replaced
1004            Track repalceTrack = getTrackByBlockingOrder(track.getBlockingOrder() - 1);
1005            if (repalceTrack != null) {
1006                repalceTrack.setBlockingOrder(track.getBlockingOrder());
1007            }
1008            track.setBlockingOrder(track.getBlockingOrder() - 1);
1009            // move the end of order
1010            if (track.getBlockingOrder() <= 0)
1011                track.setBlockingOrder(getNumberOfTracks() + 1);
1012        }
1013        resequnceTracksByBlockingOrder();
1014    }
1015
1016    public void changeTrackBlockingOrderLater(Track track) {
1017        // if track blocking order is 0, then the blocking table has never been
1018        // initialized
1019        if (track.getBlockingOrder() != 0) {
1020            // first adjust the track being replaced
1021            Track repalceTrack = getTrackByBlockingOrder(track.getBlockingOrder() + 1);
1022            if (repalceTrack != null) {
1023                repalceTrack.setBlockingOrder(track.getBlockingOrder());
1024            }
1025            track.setBlockingOrder(track.getBlockingOrder() + 1);
1026            // move the start of order
1027            if (track.getBlockingOrder() > getNumberOfTracks())
1028                track.setBlockingOrder(0);
1029        }
1030        resequnceTracksByBlockingOrder();
1031    }
1032
1033    private Track getTrackByBlockingOrder(int order) {
1034        for (Track track : getTracksList()) {
1035            if (track.getBlockingOrder() == order)
1036                return track;
1037        }
1038        return null; // not found!
1039    }
1040
1041    public boolean isTrackAtLocation(Track track) {
1042        if (track == null) {
1043            return true;
1044        }
1045        return _trackHashTable.contains(track);
1046    }
1047
1048    /**
1049     * Reset the move count for all tracks at this location
1050     */
1051    public void resetMoves() {
1052        List<Track> tracks = getTracksList();
1053        for (Track track : tracks) {
1054            track.setMoves(0);
1055        }
1056    }
1057
1058    /**
1059     * Updates a JComboBox with all of the track locations for this location.
1060     *
1061     * @param box JComboBox to be updated.
1062     */
1063    public void updateComboBox(JComboBox<Track> box) {
1064        box.removeAllItems();
1065        box.addItem(null);
1066        List<Track> tracks = getTracksByNameList(null);
1067        for (Track track : tracks) {
1068            box.addItem(track);
1069        }
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) {
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, Integer.valueOf(_poolHashTable.size() + 1),
1125                    Integer.valueOf(_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        Integer old = Integer.valueOf(_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, Integer.valueOf(_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() > 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 hasReporters() {
1310        for (Track track : getTracksList()) {
1311            if (track.getReporter() != null) {
1312                return true;
1313            }
1314        }
1315        return false;
1316    }
1317
1318    /*
1319     * set the jmri.Reporter object associated with this location.
1320     * 
1321     * @param reader jmri.Reporter object.
1322     */
1323    public void setReporter(Reporter r) {
1324        Reporter old = _reader;
1325        _reader = r;
1326        if (old != r) {
1327            setDirtyAndFirePropertyChange(LOCATION_REPORTER_CHANGED_PROPERTY, old, r);
1328        }
1329    }
1330
1331    /*
1332     * get the jmri.Reporter object associated with this location.
1333     * 
1334     * @return jmri.Reporter object.
1335     */
1336    public Reporter getReporter() {
1337        return _reader;
1338    }
1339
1340    public String getReporterName() {
1341        if (getReporter() != null) {
1342            return getReporter().getDisplayName();
1343        }
1344        return "";
1345    }
1346
1347    public void dispose() {
1348        List<Track> tracks = getTracksList();
1349        for (Track track : tracks) {
1350            deleteTrack(track);
1351        }
1352        InstanceManager.getDefault(CarTypes.class).removePropertyChangeListener(this);
1353        InstanceManager.getDefault(CarRoads.class).removePropertyChangeListener(this);
1354        InstanceManager.getDefault(EngineTypes.class).removePropertyChangeListener(this);
1355        // Change name in case object is still in use, for example Schedules
1356        setName(MessageFormat.format(Bundle.getMessage("NotValid"), new Object[]{getName()}));
1357        setDirtyAndFirePropertyChange(DISPOSE_CHANGED_PROPERTY, null, DISPOSE_CHANGED_PROPERTY);
1358    }
1359
1360    private void addPropertyChangeListeners() {
1361        InstanceManager.getDefault(CarTypes.class).addPropertyChangeListener(this);
1362        InstanceManager.getDefault(CarRoads.class).addPropertyChangeListener(this);
1363        InstanceManager.getDefault(EngineTypes.class).addPropertyChangeListener(this);
1364    }
1365
1366    /**
1367     * Construct this Entry from XML. This member has to remain synchronized
1368     * with the detailed DTD in operations-locations.dtd
1369     *
1370     * @param e Consist XML element
1371     */
1372    public Location(Element e) {
1373        Attribute a;
1374        if ((a = e.getAttribute(Xml.ID)) != null) {
1375            _id = a.getValue();
1376        } else {
1377            log.warn("no id attribute in location element when reading operations");
1378        }
1379        if ((a = e.getAttribute(Xml.NAME)) != null) {
1380            _name = a.getValue();
1381        }
1382        if ((a = e.getAttribute(Xml.DIVISION_ID)) != null) {
1383            _division = InstanceManager.getDefault(DivisionManager.class).getDivisionById(a.getValue());
1384        }
1385        // TODO remove the following 3 lines in 2023
1386        if ((a = e.getAttribute(Xml.DIVISION_ID_ERROR)) != null) {
1387            _division = InstanceManager.getDefault(DivisionManager.class).getDivisionById(a.getValue());
1388        }
1389        if ((a = e.getAttribute(Xml.DIR)) != null) {
1390            try {
1391                _trainDir = Integer.parseInt(a.getValue());
1392            } catch (NumberFormatException nfe) {
1393                log.error("Train directions isn't a vaild number for location {}", getName());
1394            }
1395        }
1396        if ((a = e.getAttribute(Xml.SWITCH_LIST)) != null) {
1397            _switchList = (a.getValue().equals(Xml.TRUE));
1398        }
1399        if ((a = e.getAttribute(Xml.SWITCH_LIST_STATE)) != null) {
1400            try {
1401                _switchListState = Integer.parseInt(a.getValue());
1402            } catch (NumberFormatException nfe) {
1403                log.error("Switch list state isn't a vaild number for location {}", getName());
1404            }
1405            if (getSwitchListState() == SW_PRINTED) {
1406                setStatus(PRINTED);
1407            }
1408        }
1409        if ((a = e.getAttribute(Xml.PRINTER_NAME)) != null) {
1410            _defaultPrinter = a.getValue();
1411        }
1412        // load train icon coordinates
1413        Attribute x;
1414        Attribute y;
1415        try {
1416            if ((x = e.getAttribute(Xml.EAST_TRAIN_ICON_X)) != null &&
1417                    (y = e.getAttribute(Xml.EAST_TRAIN_ICON_Y)) != null) {
1418                setTrainIconEast(new Point(Integer.parseInt(x.getValue()), Integer.parseInt(y.getValue())));
1419            }
1420            if ((x = e.getAttribute(Xml.WEST_TRAIN_ICON_X)) != null &&
1421                    (y = e.getAttribute(Xml.WEST_TRAIN_ICON_Y)) != null) {
1422                setTrainIconWest(new Point(Integer.parseInt(x.getValue()), Integer.parseInt(y.getValue())));
1423            }
1424            if ((x = e.getAttribute(Xml.NORTH_TRAIN_ICON_X)) != null &&
1425                    (y = e.getAttribute(Xml.NORTH_TRAIN_ICON_Y)) != null) {
1426                setTrainIconNorth(new Point(Integer.parseInt(x.getValue()), Integer.parseInt(y.getValue())));
1427            }
1428            if ((x = e.getAttribute(Xml.SOUTH_TRAIN_ICON_X)) != null &&
1429                    (y = e.getAttribute(Xml.SOUTH_TRAIN_ICON_Y)) != null) {
1430                setTrainIconSouth(new Point(Integer.parseInt(x.getValue()), Integer.parseInt(y.getValue())));
1431            }
1432
1433            if ((x = e.getAttribute(Xml.TRAIN_ICON_RANGE_X)) != null) {
1434                setTrainIconRangeX(Integer.parseInt(x.getValue()));
1435            }
1436            if ((y = e.getAttribute(Xml.TRAIN_ICON_RANGE_Y)) != null) {
1437                setTrainIconRangeY(Integer.parseInt(y.getValue()));
1438            }
1439
1440        } catch (NumberFormatException nfe) {
1441            log.error("Train icon coordinates aren't vaild for location {}", getName());
1442        }
1443
1444        if ((a = e.getAttribute(Xml.COMMENT)) != null) {
1445            _comment = a.getValue();
1446        }
1447
1448        if ((a = e.getAttribute(Xml.SWITCH_LIST_COMMENT)) != null) {
1449            _switchListComment = a.getValue();
1450        }
1451        if ((a = e.getAttribute(Xml.PHYSICAL_LOCATION)) != null) {
1452            _physicalLocation = PhysicalLocation.parse(a.getValue());
1453        }
1454        // new way of reading car types using elements added in 3.3.1
1455        if (e.getChild(Xml.TYPES) != null) {
1456            List<Element> carTypes = e.getChild(Xml.TYPES).getChildren(Xml.CAR_TYPE);
1457            String[] types = new String[carTypes.size()];
1458            for (int i = 0; i < carTypes.size(); i++) {
1459                Element type = carTypes.get(i);
1460                if ((a = type.getAttribute(Xml.NAME)) != null) {
1461                    types[i] = a.getValue();
1462                }
1463            }
1464            setTypeNames(types);
1465            List<Element> locoTypes = e.getChild(Xml.TYPES).getChildren(Xml.LOCO_TYPE);
1466            types = new String[locoTypes.size()];
1467            for (int i = 0; i < locoTypes.size(); i++) {
1468                Element type = locoTypes.get(i);
1469                if ((a = type.getAttribute(Xml.NAME)) != null) {
1470                    types[i] = a.getValue();
1471                }
1472            }
1473            setTypeNames(types);
1474        } // old way of reading car types up to version 2.99.6
1475        else if ((a = e.getAttribute(Xml.CAR_TYPES)) != null) {
1476            String names = a.getValue();
1477            String[] Types = names.split("%%"); // NOI18N
1478            setTypeNames(Types);
1479        }
1480        // early version of operations called tracks "secondary"
1481        if (e.getChildren(Xml.SECONDARY) != null) {
1482            List<Element> eTracks = e.getChildren(Xml.SECONDARY);
1483            for (Element eTrack : eTracks) {
1484                register(new Track(eTrack, this));
1485            }
1486        }
1487        if (e.getChildren(Xml.TRACK) != null) {
1488            List<Element> eTracks = e.getChildren(Xml.TRACK);
1489            log.debug("location ({}) has {} tracks", getName(), eTracks.size());
1490            for (Element eTrack : eTracks) {
1491                register(new Track(eTrack, this));
1492            }
1493        }
1494        if (e.getAttribute(Xml.READER) != null) {
1495            // @SuppressWarnings("unchecked")
1496            try {
1497                Reporter r = jmri.InstanceManager.getDefault(jmri.ReporterManager.class)
1498                        .provideReporter(e.getAttribute(Xml.READER).getValue());
1499                _reader = r;
1500            } catch (IllegalArgumentException ex) {
1501                log.warn("Not able to find reader: {} for location ({})", e.getAttribute(Xml.READER).getValue(),
1502                        getName());
1503            }
1504        }
1505        addPropertyChangeListeners();
1506    }
1507
1508    /**
1509     * Create an XML element to represent this Entry. This member has to remain
1510     * synchronized with the detailed DTD in operations-locations.dtd.
1511     *
1512     * @return Contents in a JDOM Element
1513     */
1514    public Element store() {
1515        Element e = new Element(Xml.LOCATION);
1516        e.setAttribute(Xml.ID, getId());
1517        e.setAttribute(Xml.NAME, getName());
1518        if (!getDivisionId().equals(NONE)) {
1519            e.setAttribute(Xml.DIVISION_ID, getDivisionId());
1520        }
1521        // backwards compatibility starting 2/6/2021, remove after 2023
1522        e.setAttribute(Xml.OPS, isStaging() ? STAGING : NORMAL);
1523        e.setAttribute(Xml.DIR, Integer.toString(getTrainDirections()));
1524        e.setAttribute(Xml.SWITCH_LIST, isSwitchListEnabled() ? Xml.TRUE : Xml.FALSE);
1525        if (!Setup.isSwitchListRealTime()) {
1526            e.setAttribute(Xml.SWITCH_LIST_STATE, Integer.toString(getSwitchListState()));
1527        }
1528        if (!getDefaultPrinterName().equals(NONE)) {
1529            e.setAttribute(Xml.PRINTER_NAME, getDefaultPrinterName());
1530        }
1531        if (!getTrainIconEast().equals(new Point())) {
1532            e.setAttribute(Xml.EAST_TRAIN_ICON_X, Integer.toString(getTrainIconEast().x));
1533            e.setAttribute(Xml.EAST_TRAIN_ICON_Y, Integer.toString(getTrainIconEast().y));
1534        }
1535        if (!getTrainIconWest().equals(new Point())) {
1536            e.setAttribute(Xml.WEST_TRAIN_ICON_X, Integer.toString(getTrainIconWest().x));
1537            e.setAttribute(Xml.WEST_TRAIN_ICON_Y, Integer.toString(getTrainIconWest().y));
1538        }
1539        if (!getTrainIconNorth().equals(new Point())) {
1540            e.setAttribute(Xml.NORTH_TRAIN_ICON_X, Integer.toString(getTrainIconNorth().x));
1541            e.setAttribute(Xml.NORTH_TRAIN_ICON_Y, Integer.toString(getTrainIconNorth().y));
1542        }
1543        if (!getTrainIconSouth().equals(new Point())) {
1544            e.setAttribute(Xml.SOUTH_TRAIN_ICON_X, Integer.toString(getTrainIconSouth().x));
1545            e.setAttribute(Xml.SOUTH_TRAIN_ICON_Y, Integer.toString(getTrainIconSouth().y));
1546        }
1547        if (getTrainIconRangeX() != RANGE_DEFAULT) {
1548            e.setAttribute(Xml.TRAIN_ICON_RANGE_X, Integer.toString(getTrainIconRangeX()));
1549        }
1550        if (getTrainIconRangeY() != RANGE_DEFAULT) {
1551            e.setAttribute(Xml.TRAIN_ICON_RANGE_Y, Integer.toString(getTrainIconRangeY()));
1552        }
1553        if (_reader != null) {
1554            e.setAttribute(Xml.READER, _reader.getDisplayName());
1555        }
1556        // build list of rolling stock types for this location
1557        String[] types = getTypeNames();
1558        // new way of saving car types
1559        Element eTypes = new Element(Xml.TYPES);
1560        for (String type : types) {
1561            // don't save types that have been deleted by user
1562            if (InstanceManager.getDefault(EngineTypes.class).containsName(type)) {
1563                Element eType = new Element(Xml.LOCO_TYPE);
1564                eType.setAttribute(Xml.NAME, type);
1565                eTypes.addContent(eType);
1566            } else if (InstanceManager.getDefault(CarTypes.class).containsName(type)) {
1567                Element eType = new Element(Xml.CAR_TYPE);
1568                eType.setAttribute(Xml.NAME, type);
1569                eTypes.addContent(eType);
1570            }
1571        }
1572        e.addContent(eTypes);
1573
1574        // save physical location if not default
1575        if (getPhysicalLocation() != null && !getPhysicalLocation().equals(PhysicalLocation.Origin)) {
1576            e.setAttribute(Xml.PHYSICAL_LOCATION, getPhysicalLocation().toString());
1577        }
1578
1579        e.setAttribute(Xml.COMMENT, getCommentWithColor());
1580        e.setAttribute(Xml.SWITCH_LIST_COMMENT, getSwitchListCommentWithColor());
1581
1582        List<Track> tracks = getTracksByIdList();
1583        for (Track track : tracks) {
1584            e.addContent(track.store());
1585        }
1586
1587        return e;
1588    }
1589
1590    private void replaceType(String oldType, String newType) {
1591        if (acceptsTypeName(oldType)) {
1592            if (newType != null) {
1593                addTypeName(newType);
1594            }
1595            // now adjust tracks
1596            List<Track> tracks = getTracksList();
1597            for (Track track : tracks) {
1598                if (track.isTypeNameAccepted(oldType)) {
1599                    track.deleteTypeName(oldType);
1600                    if (newType != null) {
1601                        track.addTypeName(newType);
1602                    }
1603                }
1604                // adjust custom loads
1605                String[] loadNames = track.getLoadNames();
1606                for (String load : loadNames) {
1607                    String[] splitLoad = load.split(CarLoad.SPLIT_CHAR);
1608                    if (splitLoad.length > 1) {
1609                        if (splitLoad[0].equals(oldType)) {
1610                            track.deleteLoadName(load);
1611                            if (newType != null) {
1612                                load = newType + CarLoad.SPLIT_CHAR + splitLoad[1];
1613                                track.addLoadName(load);
1614                            }
1615                        }
1616                    }
1617                }
1618                loadNames = track.getShipLoadNames();
1619                for (String load : loadNames) {
1620                    String[] splitLoad = load.split(CarLoad.SPLIT_CHAR);
1621                    if (splitLoad.length > 1) {
1622                        if (splitLoad[0].equals(oldType)) {
1623                            track.deleteShipLoadName(load);
1624                            if (newType != null) {
1625                                load = newType + CarLoad.SPLIT_CHAR + splitLoad[1];
1626                                track.addShipLoadName(load);
1627                            }
1628                        }
1629                    }
1630                }
1631            }
1632            deleteTypeName(oldType);
1633        }
1634    }
1635
1636    private void replaceRoad(String oldRoad, String newRoad) {
1637        // now adjust any track locations
1638        List<Track> tracks = getTracksList();
1639        for (Track track : tracks) {
1640            if (track.containsRoadName(oldRoad)) {
1641                track.deleteRoadName(oldRoad);
1642                if (newRoad != null) {
1643                    track.addRoadName(newRoad);
1644                }
1645            }
1646        }
1647    }
1648
1649    @Override
1650    public void propertyChange(java.beans.PropertyChangeEvent e) {
1651        if (Control.SHOW_PROPERTY) {
1652            log.debug("Property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e.getOldValue(),
1653                    e.getNewValue());
1654        }
1655        // update length of tracks at this location if track length changes
1656        if (e.getPropertyName().equals(Track.LENGTH_CHANGED_PROPERTY)) {
1657            setLength(getLength() -
1658                    Integer.parseInt((String) e.getOldValue()) +
1659                    Integer.parseInt((String) e.getNewValue()));
1660        }
1661        // if a track type change, must update all tables
1662        if (e.getPropertyName().equals(Track.TRACK_TYPE_CHANGED_PROPERTY)) {
1663            setDirtyAndFirePropertyChange(TRACK_LISTLENGTH_CHANGED_PROPERTY, null, null);
1664        }
1665        if (e.getPropertyName().equals(CarTypes.CARTYPES_NAME_CHANGED_PROPERTY) ||
1666                e.getPropertyName().equals(CarTypes.CARTYPES_CHANGED_PROPERTY) ||
1667                e.getPropertyName().equals(EngineTypes.ENGINETYPES_NAME_CHANGED_PROPERTY)) {
1668            replaceType((String) e.getOldValue(), (String) e.getNewValue());
1669        }
1670        if (e.getPropertyName().equals(CarRoads.CARROADS_NAME_CHANGED_PROPERTY)) {
1671            replaceRoad((String) e.getOldValue(), (String) e.getNewValue());
1672        }
1673    }
1674
1675    protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
1676        InstanceManager.getDefault(LocationManagerXml.class).setDirty(true);
1677        firePropertyChange(p, old, n);
1678    }
1679
1680    private final static Logger log = LoggerFactory.getLogger(Location.class);
1681
1682}