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