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