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