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.rollingstock.cars.CarLoad;
015import jmri.jmrit.operations.setup.OperationsSetupXml;
016
017/**
018 * Manages locations.
019 *
020 * @author Bob Jacobsen Copyright (C) 2003
021 * @author Daniel Boudreau Copyright (C) 2008, 2009, 2013, 2014
022 */
023public class LocationManager extends PropertyChangeSupport implements InstanceManagerAutoDefault, InstanceManagerAutoInitialize, PropertyChangeListener {
024
025    public static final String LISTLENGTH_CHANGED_PROPERTY = "locationsListLength"; // NOI18N
026    
027    protected boolean _showId = false; // when true show location ids 
028
029    public LocationManager() {
030    }
031
032    private int _id = 0;
033
034    public void dispose() {
035        _locationHashTable.clear();
036        _id = 0;
037    }
038
039    protected Hashtable<String, Location> _locationHashTable = new Hashtable<String, Location>();
040
041    /**
042     * @return Number of locations
043     */
044    public int getNumberOfLocations() {
045        return _locationHashTable.size();
046    }
047
048    /**
049     * @param name The string name of the Location to get.
050     * @return requested Location object or null if none exists
051     */
052    public Location getLocationByName(String name) {
053        Location location;
054        Enumeration<Location> en = _locationHashTable.elements();
055        while (en.hasMoreElements()) {
056            location = en.nextElement();
057            if (location.getName().equals(name)) {
058                return location;
059            }
060        }
061        return null;
062    }
063
064    public Location getLocationById(String id) {
065        return _locationHashTable.get(id);
066    }
067    
068    /**
069     * Used to determine if a division name has been assigned to a location
070     * @return true if a location has a division name
071     */
072    public boolean hasDivisions() {
073        for (Location location : getList()) {
074            if (location.getDivision() != null) {
075                return true;
076            }
077        }
078        return false;
079    }
080    
081    public boolean hasWork() {
082        for (Location location : getList()) {
083            if (location.hasWork()) {
084                return true;
085            }
086        }
087        return false;
088    }
089    
090    /**
091     * Used to determine if a reporter has been assigned to a location
092     * @return true if a location has a RFID reporter
093     */
094    public boolean hasReporters() {
095        for (Location location : getList()) {
096            if (location.getReporter() != null) {
097                return true;
098            }
099        }
100        return false;
101    }
102    
103    public void setShowIdEnabled(boolean showId) {
104        _showId = showId;
105    }
106    
107    public boolean isShowIdEnabled() {
108        return _showId;
109    }
110
111    /**
112     * Request a location associated with a given reporter.
113     *
114     * @param r Reporter object associated with desired location.
115     * @return requested Location object or null if none exists
116     */
117    public Location getLocationByReporter(Reporter r) {
118        for (Location location : _locationHashTable.values()) {
119            if (location.getReporter() != null) {
120                if (location.getReporter().equals(r)) {
121                    return location;
122                }
123            }
124        }
125        return null;
126    }
127
128    /**
129     * Request a track associated with a given reporter.
130     *
131     * @param r Reporter object associated with desired location.
132     * @return requested Location object or null if none exists
133     */
134    public Track getTrackByReporter(Reporter r) {
135        for (Track track : getTracks(null)) {
136            if (track.getReporter() != null) {
137                if (track.getReporter().equals(r)) {
138                    return track;
139                }
140            }
141        }
142        return null;
143    }
144
145    /**
146     * Finds an existing location or creates a new location if needed requires
147     * location's name creates a unique id for this location
148     *
149     * @param name The string name for a new Location.
150     *
151     *
152     * @return new location or existing location
153     */
154    public Location newLocation(String name) {
155        Location location = getLocationByName(name);
156        if (location == null) {
157            _id++;
158            location = new Location(Integer.toString(_id), name);
159            Integer oldSize = Integer.valueOf(_locationHashTable.size());
160            _locationHashTable.put(location.getId(), location);
161            resetNameLengths();
162            setDirtyAndFirePropertyChange(LISTLENGTH_CHANGED_PROPERTY, oldSize,
163                    Integer.valueOf(_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        Integer oldSize = Integer.valueOf(_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, Integer.valueOf(_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        Integer oldSize = Integer.valueOf(_locationHashTable.size());
195        _locationHashTable.remove(location.getId());
196        setDirtyAndFirePropertyChange(LISTLENGTH_CHANGED_PROPERTY, oldSize, Integer.valueOf(_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
316     * are not 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     * @return locations for this railroad
359     */
360    public JComboBox<Location> getComboBox() {
361        JComboBox<Location> box = new JComboBox<>();
362        updateComboBox(box);
363        return box;
364    }
365
366    /**
367     * Updates JComboBox alphabetically with a list of locations.
368     * @param box The JComboBox to update.
369     */
370    public void updateComboBox(JComboBox<Location> box) {
371        box.removeAllItems();
372        box.addItem(null);
373        for (Location loc : getLocationsByNameList()) {
374            box.addItem(loc);
375        }
376    }
377
378    /**
379     * Replace all track car load names for a given type of car
380     * 
381     * @param type type of car
382     * @param oldLoadName load name to replace
383     * @param newLoadName new load name
384     */
385    public void replaceLoad(String type, String oldLoadName, String newLoadName) {
386        List<Location> locs = getList();
387        for (Location loc : locs) {
388            // now adjust tracks
389            List<Track> tracks = loc.getTracksList();
390            for (Track track : tracks) {
391                for (String loadName : track.getLoadNames()) {
392                    if (loadName.equals(oldLoadName)) {
393                        track.deleteLoadName(oldLoadName);
394                        if (newLoadName != null) {
395                            track.addLoadName(newLoadName);
396                        }
397                    }
398                    // adjust combination car type and load name
399                    String[] splitLoad = loadName.split(CarLoad.SPLIT_CHAR);
400                    if (splitLoad.length > 1) {
401                        if (splitLoad[0].equals(type) && splitLoad[1].equals(oldLoadName)) {
402                            track.deleteLoadName(loadName);
403                            if (newLoadName != null) {
404                                track.addLoadName(type + CarLoad.SPLIT_CHAR + newLoadName);
405                            }
406                        }
407                    }
408                }
409                // now adjust ship load names
410                for (String loadName : track.getShipLoadNames()) {
411                    if (loadName.equals(oldLoadName)) {
412                        track.deleteShipLoadName(oldLoadName);
413                        if (newLoadName != null) {
414                            track.addShipLoadName(newLoadName);
415                        }
416                    }
417                    // adjust combination car type and load name
418                    String[] splitLoad = loadName.split(CarLoad.SPLIT_CHAR);
419                    if (splitLoad.length > 1) {
420                        if (splitLoad[0].equals(type) && splitLoad[1].equals(oldLoadName)) {
421                            track.deleteShipLoadName(loadName);
422                            if (newLoadName != null) {
423                                track.addShipLoadName(type + CarLoad.SPLIT_CHAR + newLoadName);
424                            }
425                        }
426                    }
427                }
428            }
429        }
430    }
431
432    protected int _maxLocationNameLength = 0;
433    protected int _maxTrackNameLength = 0;
434    protected int _maxLocationAndTrackNameLength = 0;
435
436    public void resetNameLengths() {
437        _maxLocationNameLength = 0;
438        _maxTrackNameLength = 0;
439        _maxLocationAndTrackNameLength = 0;
440    }
441
442    public int getMaxLocationNameLength() {
443        calculateMaxNameLengths();
444        return _maxLocationNameLength;
445    }
446
447    public int getMaxTrackNameLength() {
448        calculateMaxNameLengths();
449        return _maxTrackNameLength;
450    }
451
452    public int getMaxLocationAndTrackNameLength() {
453        calculateMaxNameLengths();
454        return _maxLocationAndTrackNameLength;
455    }
456
457    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "SLF4J_FORMAT_SHOULD_BE_CONST",
458            justification = "I18N of Info Message")
459    private void calculateMaxNameLengths() {
460        if (_maxLocationNameLength != 0) // only do this once
461        {
462            return;
463        }
464        String maxTrackName = "";
465        String maxLocNameForTrack = "";
466        String maxLocationName = "";
467        String maxLocationAndTrackName = "";
468        for (Track track : getTracks(null)) {
469            if (track.getSplitName().length() > _maxTrackNameLength) {
470                maxTrackName = track.getName();
471                maxLocNameForTrack = track.getLocation().getName();
472                _maxTrackNameLength = track.getSplitName().length();
473            }
474            if (track.getLocation().getSplitName().length() > _maxLocationNameLength) {
475                maxLocationName = track.getLocation().getName();
476                _maxLocationNameLength = track.getLocation().getSplitName().length();
477            }
478            if (track.getLocation().getSplitName().length()
479                    + track.getSplitName().length() > _maxLocationAndTrackNameLength) {
480                maxLocationAndTrackName = track.getLocation().getName() + ", " + track.getName();
481                _maxLocationAndTrackNameLength = track.getLocation().getSplitName().length()
482                        + track.getSplitName().length();
483            }
484        }
485        log.info(Bundle.getMessage("InfoMaxTrackName", maxTrackName, _maxTrackNameLength, maxLocNameForTrack));
486        log.info(Bundle.getMessage("InfoMaxLocationName", maxLocationName, _maxLocationNameLength));
487        log.info(Bundle.getMessage("InfoMaxLocAndTrackName", maxLocationAndTrackName, _maxLocationAndTrackNameLength));
488    }
489
490    /**
491     * Load the locations from a xml file.
492     * @param root xml file
493     */
494    public void load(Element root) {
495        if (root.getChild(Xml.LOCATIONS) != null) {
496            List<Element> locs = root.getChild(Xml.LOCATIONS).getChildren(Xml.LOCATION);
497            log.debug("readFile sees {} locations", locs.size());
498            for (Element loc : locs) {
499                register(new Location(loc));
500            }
501        }
502    }
503
504    public void store(Element root) {
505        Element values;
506        root.addContent(values = new Element(Xml.LOCATIONS));
507        // add entries
508        List<Location> locationList = getLocationsByIdList();
509        for (Location loc : locationList) {
510            values.addContent(loc.store());
511        }
512    }
513
514    /**
515     * There aren't any current property changes being monitored.
516     */
517    @Override
518    public void propertyChange(java.beans.PropertyChangeEvent e) {
519        log.debug("LocationManager sees property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e
520                .getOldValue(), e.getNewValue()); // NOI18N
521    }
522
523    protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
524        // set dirty
525        InstanceManager.getDefault(LocationManagerXml.class).setDirty(true);
526        firePropertyChange(p, old, n);
527    }
528
529    private final static Logger log = LoggerFactory.getLogger(LocationManager.class);
530
531    @Override
532    public void initialize() {
533        InstanceManager.getDefault(OperationsSetupXml.class); // load setup
534        InstanceManager.getDefault(LocationManagerXml.class); // load locations
535    }
536}