001package jmri.jmrit.operations.locations;
002
003import java.beans.PropertyChangeListener;
004import java.util.*;
005
006import javax.swing.JComboBox;
007
008import org.jdom2.Element;
009import org.slf4j.Logger;
010import org.slf4j.LoggerFactory;
011
012import jmri.*;
013import jmri.beans.PropertyChangeSupport;
014import jmri.jmrit.operations.OperationsPanel;
015import jmri.jmrit.operations.rollingstock.cars.CarLoad;
016import jmri.jmrit.operations.setup.OperationsSetupXml;
017
018/**
019 * Manages locations.
020 *
021 * @author Bob Jacobsen Copyright (C) 2003
022 * @author Daniel Boudreau Copyright (C) 2008, 2009, 2013, 2014
023 */
024public class LocationManager extends PropertyChangeSupport implements InstanceManagerAutoDefault, InstanceManagerAutoInitialize, PropertyChangeListener {
025
026    public static final String LISTLENGTH_CHANGED_PROPERTY = "locationsListLength"; // NOI18N
027
028    protected boolean _showId = false; // when true show location ids 
029
030    public LocationManager() {
031    }
032
033    private int _id = 0;
034
035    public void dispose() {
036        _locationHashTable.clear();
037        _id = 0;
038    }
039
040    protected Hashtable<String, Location> _locationHashTable = new Hashtable<String, Location>();
041
042    /**
043     * @return Number of locations
044     */
045    public int getNumberOfLocations() {
046        return _locationHashTable.size();
047    }
048
049    /**
050     * @param name The string name of the Location to get.
051     * @return requested Location object or null if none exists
052     */
053    public Location getLocationByName(String name) {
054        Location location;
055        Enumeration<Location> en = _locationHashTable.elements();
056        while (en.hasMoreElements()) {
057            location = en.nextElement();
058            if (location.getName().equals(name)) {
059                return location;
060            }
061        }
062        return null;
063    }
064
065    public Location getLocationById(String id) {
066        return _locationHashTable.get(id);
067    }
068
069    /**
070     * Used to determine if a division name has been assigned to a location
071     * 
072     * @return true if a location has a division name
073     */
074    public boolean hasDivisions() {
075        for (Location location : getList()) {
076            if (location.getDivision() != null) {
077                return true;
078            }
079        }
080        return false;
081    }
082
083    public boolean hasWork() {
084        for (Location location : getList()) {
085            if (location.hasWork()) {
086                return true;
087            }
088        }
089        return false;
090    }
091
092    /**
093     * Used to determine if a reporter has been assigned to a location
094     * 
095     * @return true if a location has a RFID reporter
096     */
097    public boolean hasReporters() {
098        for (Location location : getList()) {
099            if (location.getReporter() != null) {
100                return true;
101            }
102        }
103        return false;
104    }
105
106    public void setShowIdEnabled(boolean showId) {
107        _showId = showId;
108    }
109
110    public boolean isShowIdEnabled() {
111        return _showId;
112    }
113
114    /**
115     * Request a location associated with a given reporter.
116     *
117     * @param r Reporter object associated with desired location.
118     * @return requested Location object or null if none exists
119     */
120    public Location getLocationByReporter(Reporter r) {
121        for (Location location : _locationHashTable.values()) {
122            if (location.getReporter() != null) {
123                if (location.getReporter().equals(r)) {
124                    return location;
125                }
126            }
127        }
128        return null;
129    }
130
131    /**
132     * Request a track associated with a given reporter.
133     *
134     * @param r Reporter object associated with desired location.
135     * @return requested Location object or null if none exists
136     */
137    public Track getTrackByReporter(Reporter r) {
138        for (Track track : getTracks(null)) {
139            if (track.getReporter() != null) {
140                if (track.getReporter().equals(r)) {
141                    return track;
142                }
143            }
144        }
145        return null;
146    }
147
148    /**
149     * Finds an existing location or creates a new location if needed requires
150     * location's name creates a unique id for this location
151     *
152     * @param name The string name for a new Location.
153     * @return new location or existing location
154     */
155    public Location newLocation(String name) {
156        Location location = getLocationByName(name);
157        if (location == null) {
158            _id++;
159            location = new Location(Integer.toString(_id), name);
160            int oldSize = _locationHashTable.size();
161            _locationHashTable.put(location.getId(), location);
162            resetNameLengths();
163            setDirtyAndFirePropertyChange(LISTLENGTH_CHANGED_PROPERTY, oldSize, _locationHashTable.size());
164        }
165        return location;
166    }
167
168    /**
169     * Remember a NamedBean Object created outside the manager.
170     *
171     * @param location The Location to add.
172     */
173    public void register(Location location) {
174        int oldSize = _locationHashTable.size();
175        _locationHashTable.put(location.getId(), location);
176        // find last id created
177        int id = Integer.parseInt(location.getId());
178        if (id > _id) {
179            _id = id;
180        }
181        setDirtyAndFirePropertyChange(LISTLENGTH_CHANGED_PROPERTY, oldSize, _locationHashTable.size());
182    }
183
184    /**
185     * Forget a NamedBean Object created outside the manager.
186     *
187     * @param location The Location to delete.
188     */
189    public void deregister(Location location) {
190        if (location == null) {
191            return;
192        }
193        location.dispose();
194        int oldSize = _locationHashTable.size();
195        _locationHashTable.remove(location.getId());
196        setDirtyAndFirePropertyChange(LISTLENGTH_CHANGED_PROPERTY, oldSize, _locationHashTable.size());
197    }
198
199    /**
200     * Sort by location name
201     *
202     * @return list of locations ordered by name
203     */
204    public List<Location> getLocationsByNameList() {
205        // first get id list
206        List<Location> sortList = getList();
207        // now re-sort
208        List<Location> out = new ArrayList<Location>();
209        for (Location location : sortList) {
210            for (int j = 0; j < out.size(); j++) {
211                if (location.getName().compareToIgnoreCase(out.get(j).getName()) < 0) {
212                    out.add(j, location);
213                    break;
214                }
215            }
216            if (!out.contains(location)) {
217                out.add(location);
218            }
219        }
220        return out;
221    }
222
223    /**
224     * Get unique locations list by location name.
225     *
226     * @return list of locations ordered by name. Locations with "similar" names
227     *         to the primary location are not returned. Also checks and updates
228     *         the primary location for any changes to the other "similar"
229     *         locations.
230     */
231    public List<Location> getUniqueLocationsByNameList() {
232        List<Location> locations = getLocationsByNameList();
233        List<Location> out = new ArrayList<Location>();
234        Location mainLocation = null;
235
236        // also update the primary location for locations with similar names
237        for (Location location : locations) {
238            String name = location.getSplitName();
239            if (mainLocation != null && mainLocation.getSplitName().equals(name)) {
240                location.setSwitchListEnabled(mainLocation.isSwitchListEnabled());
241                if (mainLocation.isSwitchListEnabled() && location.getStatus().equals(Location.MODIFIED)) {
242                    mainLocation.setStatus(Location.MODIFIED); // we need to update the primary location
243                    location.setStatus(Location.UPDATED); // and clear the secondaries
244                }
245                continue;
246            }
247            mainLocation = location;
248            out.add(location);
249        }
250        return out;
251    }
252
253    /**
254     * Sort by location number, number can alpha numeric
255     *
256     * @return list of locations ordered by id numbers
257     */
258    public List<Location> getLocationsByIdList() {
259        List<Location> sortList = getList();
260        // now re-sort
261        List<Location> out = new ArrayList<Location>();
262        for (Location location : sortList) {
263            for (int j = 0; j < out.size(); j++) {
264                try {
265                    if (Integer.parseInt(location.getId()) < Integer.parseInt(out.get(j).getId())) {
266                        out.add(j, location);
267                        break;
268                    }
269                } catch (NumberFormatException e) {
270                    log.debug("list id number isn't a number");
271                }
272            }
273            if (!out.contains(location)) {
274                out.add(location);
275            }
276        }
277        return out;
278    }
279
280    /**
281     * Gets an unsorted list of all locations.
282     *
283     * @return All locations.
284     */
285    public List<Location> getList() {
286        List<Location> out = new ArrayList<Location>();
287        Enumeration<Location> en = _locationHashTable.elements();
288        while (en.hasMoreElements()) {
289            out.add(en.nextElement());
290        }
291        return out;
292    }
293
294    /**
295     * Returns all tracks of type
296     *
297     * @param type Spur (Track.SPUR), Yard (Track.YARD), Interchange
298     *             (Track.INTERCHANGE), Staging (Track.STAGING), or null
299     *             (returns all track types)
300     * @return List of tracks
301     */
302    public List<Track> getTracks(String type) {
303        List<Location> sortList = getList();
304        List<Track> trackList = new ArrayList<Track>();
305        for (Location location : sortList) {
306            List<Track> tracks = location.getTracksByNameList(type);
307            for (Track track : tracks) {
308                trackList.add(track);
309            }
310        }
311        return trackList;
312    }
313
314    /**
315     * Returns all tracks of type sorted by use. Alternate tracks are not
316     * included.
317     *
318     * @param type Spur (Track.SPUR), Yard (Track.YARD), Interchange
319     *             (Track.INTERCHANGE), Staging (Track.STAGING), or null
320     *             (returns all track types)
321     * @return List of tracks ordered by use
322     */
323    public List<Track> getTracksByMoves(String type) {
324        List<Track> trackList = getTracks(type);
325        // now re-sort
326        List<Track> moveList = new ArrayList<Track>();
327        for (Track track : trackList) {
328            boolean locAdded = false;
329            if (track.isAlternate()) {
330                continue;
331            }
332            for (int j = 0; j < moveList.size(); j++) {
333                if (track.getMoves() < moveList.get(j).getMoves()) {
334                    moveList.add(j, track);
335                    locAdded = true;
336                    break;
337                }
338            }
339            if (!locAdded) {
340                moveList.add(track);
341            }
342        }
343        return moveList;
344    }
345
346    /**
347     * Sets move count to 0 for all tracks
348     */
349    public void resetMoves() {
350        List<Location> locations = getList();
351        for (Location loc : locations) {
352            loc.resetMoves();
353        }
354    }
355
356    /**
357     * Returns a JComboBox with locations sorted alphabetically.
358     * 
359     * @return locations for this railroad
360     */
361    public JComboBox<Location> getComboBox() {
362        JComboBox<Location> box = new JComboBox<>();
363        updateComboBox(box);
364        OperationsPanel.padComboBox(box, getMaxLocationNameLength());
365        return box;
366    }
367
368    /**
369     * Updates JComboBox alphabetically with a list of locations.
370     * 
371     * @param box The JComboBox to update.
372     */
373    public void updateComboBox(JComboBox<Location> box) {
374        box.removeAllItems();
375        box.addItem(null);
376        for (Location loc : getLocationsByNameList()) {
377            box.addItem(loc);
378        }
379    }
380
381    /**
382     * Replace all track car load names for a given type of car
383     * 
384     * @param type        type of car
385     * @param oldLoadName load name to replace
386     * @param newLoadName new load name
387     */
388    public void replaceLoad(String type, String oldLoadName, String newLoadName) {
389        List<Location> locs = getList();
390        for (Location loc : locs) {
391            // now adjust tracks
392            List<Track> tracks = loc.getTracksList();
393            for (Track track : tracks) {
394                for (String loadName : track.getLoadNames()) {
395                    if (loadName.equals(oldLoadName)) {
396                        track.deleteLoadName(oldLoadName);
397                        if (newLoadName != null) {
398                            track.addLoadName(newLoadName);
399                        }
400                    }
401                    // adjust combination car type and load name
402                    String[] splitLoad = loadName.split(CarLoad.SPLIT_CHAR);
403                    if (splitLoad.length > 1) {
404                        if (splitLoad[0].equals(type) && splitLoad[1].equals(oldLoadName)) {
405                            track.deleteLoadName(loadName);
406                            if (newLoadName != null) {
407                                track.addLoadName(type + CarLoad.SPLIT_CHAR + newLoadName);
408                            }
409                        }
410                    }
411                }
412                // now adjust ship load names
413                for (String loadName : track.getShipLoadNames()) {
414                    if (loadName.equals(oldLoadName)) {
415                        track.deleteShipLoadName(oldLoadName);
416                        if (newLoadName != null) {
417                            track.addShipLoadName(newLoadName);
418                        }
419                    }
420                    // adjust combination car type and load name
421                    String[] splitLoad = loadName.split(CarLoad.SPLIT_CHAR);
422                    if (splitLoad.length > 1) {
423                        if (splitLoad[0].equals(type) && splitLoad[1].equals(oldLoadName)) {
424                            track.deleteShipLoadName(loadName);
425                            if (newLoadName != null) {
426                                track.addShipLoadName(type + CarLoad.SPLIT_CHAR + newLoadName);
427                            }
428                        }
429                    }
430                }
431            }
432        }
433    }
434
435    protected int _maxLocationNameLength = 0;
436    protected int _maxTrackNameLength = 0;
437    protected int _maxLocationAndTrackNameLength = 0;
438
439    public void resetNameLengths() {
440        _maxLocationNameLength = 0;
441        _maxTrackNameLength = 0;
442        _maxLocationAndTrackNameLength = 0;
443    }
444
445    public int getMaxLocationNameLength() {
446        calculateMaxNameLengths();
447        return _maxLocationNameLength;
448    }
449
450    public int getMaxTrackNameLength() {
451        calculateMaxNameLengths();
452        return _maxTrackNameLength;
453    }
454
455    public int getMaxLocationAndTrackNameLength() {
456        calculateMaxNameLengths();
457        return _maxLocationAndTrackNameLength;
458    }
459
460    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "SLF4J_FORMAT_SHOULD_BE_CONST",
461            justification = "I18N of Info Message")
462    private void calculateMaxNameLengths() {
463        if (_maxLocationNameLength != 0) // only do this once
464        {
465            return;
466        }
467        String maxTrackName = "";
468        String maxLocNameForTrack = "";
469        String maxLocationName = "";
470        String maxLocationAndTrackName = "";
471        for (Track track : getTracks(null)) {
472            if (track.getSplitName().length() > _maxTrackNameLength) {
473                maxTrackName = track.getName();
474                maxLocNameForTrack = track.getLocation().getName();
475                _maxTrackNameLength = track.getSplitName().length();
476            }
477            if (track.getLocation().getSplitName().length() > _maxLocationNameLength) {
478                maxLocationName = track.getLocation().getName();
479                _maxLocationNameLength = track.getLocation().getSplitName().length();
480            }
481            if (track.getLocation().getSplitName().length() +
482                    track.getSplitName().length() > _maxLocationAndTrackNameLength) {
483                maxLocationAndTrackName = track.getLocation().getName() + ", " + track.getName();
484                _maxLocationAndTrackNameLength =
485                        track.getLocation().getSplitName().length() + track.getSplitName().length();
486            }
487        }
488        log.info(Bundle.getMessage("InfoMaxTrackName", maxTrackName, _maxTrackNameLength, maxLocNameForTrack));
489        log.info(Bundle.getMessage("InfoMaxLocationName", maxLocationName, _maxLocationNameLength));
490        log.info(Bundle.getMessage("InfoMaxLocAndTrackName", maxLocationAndTrackName, _maxLocationAndTrackNameLength));
491    }
492
493    /**
494     * Load the locations from a xml file.
495     * 
496     * @param root xml file
497     */
498    public void load(Element root) {
499        if (root.getChild(Xml.LOCATIONS) != null) {
500            List<Element> locs = root.getChild(Xml.LOCATIONS).getChildren(Xml.LOCATION);
501            log.debug("readFile sees {} locations", locs.size());
502            for (Element loc : locs) {
503                register(new Location(loc));
504            }
505        }
506    }
507
508    public void store(Element root) {
509        Element values;
510        root.addContent(values = new Element(Xml.LOCATIONS));
511        // add entries
512        List<Location> locationList = getLocationsByIdList();
513        for (Location loc : locationList) {
514            values.addContent(loc.store());
515        }
516    }
517
518    /**
519     * There aren't any current property changes being monitored.
520     */
521    @Override
522    public void propertyChange(java.beans.PropertyChangeEvent e) {
523        log.debug("LocationManager sees property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e
524                .getOldValue(), e.getNewValue()); // NOI18N
525    }
526
527    protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
528        // set dirty
529        InstanceManager.getDefault(LocationManagerXml.class).setDirty(true);
530        firePropertyChange(p, old, n);
531    }
532
533    private static final Logger log = LoggerFactory.getLogger(LocationManager.class);
534
535    @Override
536    public void initialize() {
537        InstanceManager.getDefault(OperationsSetupXml.class); // load setup
538        InstanceManager.getDefault(LocationManagerXml.class); // load locations
539    }
540}