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