001package jmri.jmrit.operations.rollingstock.engines;
002
003import java.beans.PropertyChangeEvent;
004import java.util.List;
005
006import jmri.InstanceManager;
007import jmri.jmrit.operations.locations.Location;
008import jmri.jmrit.operations.locations.Track;
009import jmri.jmrit.operations.rollingstock.RollingStock;
010import jmri.jmrit.operations.routes.RouteLocation;
011import jmri.jmrit.operations.trains.Train;
012import jmri.jmrit.roster.Roster;
013import jmri.jmrit.roster.RosterEntry;
014
015import org.slf4j.Logger;
016import org.slf4j.LoggerFactory;
017
018/**
019 * Represents a locomotive on the layout
020 *
021 * @author Daniel Boudreau (C) Copyright 2008, 2026
022 */
023public class Engine extends RollingStock {
024
025    public static final int NCE_REAR_BLOCK_NUMBER = 8;
026    public static final int B_UNIT_BLOCKING = 10; // block B Units after NCE Consists
027    public static final String HP_CHANGED_PROPERTY = "hp"; // NOI18N
028
029    private Consist _consist = null;
030    private String _model = NONE;
031
032    EngineModels engineModels = InstanceManager.getDefault(EngineModels.class);
033    
034    public Engine() {
035        super();
036    }
037
038    public Engine(String road, String number) {
039        super(road, number);
040        log.debug("New engine ({} {})", road, number);
041    }
042
043    @Override
044    public Engine copy() {
045        Engine eng = new Engine();
046        super.copy(eng);
047        eng.setModel(getModel());
048        eng.setBunit(isBunit());
049        return eng;
050    }
051    
052    /**
053     * Set the locomotive's model. Note a model has only one length, type, and
054     * horsepower rating.
055     *
056     * @param model The string model name.
057     *
058     */
059    public void setModel(String model) {
060        String old = _model;
061        _model = model;
062        if (!old.equals(model)) {
063            setDirtyAndFirePropertyChange("engine model", old, model); // NOI18N
064        }
065    }
066
067    public String getModel() {
068        return _model;
069    }
070
071    /**
072     * Set the locomotive type for this locomotive's model
073     *
074     * @param type Locomotive type: Steam, Diesel, Gas Turbine, etc.
075     */
076    @Override
077    public void setTypeName(String type) {
078        if (getModel() == null || getModel().equals(NONE)) {
079            return;
080        }
081        String old = getTypeName();
082        engineModels.setModelType(getModel(), type);
083        if (!old.equals(type)) {
084            setDirtyAndFirePropertyChange(TYPE_CHANGED_PROPERTY, old, type);
085        }
086    }
087
088    @Override
089    public String getTypeName() {
090        String type = engineModels.getModelType(getModel());
091        if (type == null) {
092            type = super.getTypeName();
093        }
094        return type;
095    }
096
097    /**
098     * Set the locomotive horsepower rating for this locomotive's model
099     *
100     * @param hp locomotive horsepower
101     */
102    public void setHp(String hp) {
103        if (getModel() == null || getModel().equals(NONE)) {
104            return;
105        }
106        String old = getHp();
107        engineModels.setModelHorsepower(getModel(), hp);
108        if (!old.equals(hp)) {
109            setDirtyAndFirePropertyChange(HP_CHANGED_PROPERTY, old, hp);
110        }
111    }
112
113    public String getHp() {
114        String hp = engineModels.getModelHorsepower(getModel());
115        if (hp == null) {
116            hp = NONE;
117        }
118        return hp;
119    }
120
121    public int getHpInteger() {
122        try {
123            return Integer.parseInt(getHp());
124        } catch (NumberFormatException e) {
125            log.debug("Locomotive ({}) horsepower ({}) isn't a number", toString(), getHp());
126            return 0;
127        }
128    }
129
130    /**
131     * Set the locomotive length for this locomotive's model
132     *
133     * @param length locomotive length
134     */
135    @Override
136    public void setLength(String length) {
137        super.setLength(length);
138        if (getModel() == null || getModel().equals(NONE)) {
139            return;
140        }
141        engineModels.setModelLength(getModel(), length);
142    }
143
144    @Override
145    public String getLength() {
146        String length = super.getLength();
147        if (getModel() != null && !getModel().equals(NONE)) {
148            length = engineModels.getModelLength(getModel());
149        }
150        if (length == null) {
151            length = NONE;
152        }
153        if (!length.equals(_length)) {
154            // return "old" length, used for track reserve changes
155            if (_lengthChange) {
156                return _length;
157            }
158            log.debug("Loco ({}) length ({}) has been modified from ({})", toString(), length, _length);
159            super.setLength(length); // adjust track lengths
160        }
161        return length;
162    }
163
164    /**
165     * Set the locomotive weight for this locomotive's model
166     *
167     * @param weight locomotive weight
168     */
169    @Override
170    public void setWeightTons(String weight) {
171        if (getModel() == null || getModel().equals(NONE)) {
172            return;
173        }
174        String old = getWeightTons();
175        super.setWeightTons(weight);
176        engineModels.setModelWeight(getModel(), weight);
177        if (!old.equals(weight)) {
178            setDirtyAndFirePropertyChange("Engine Weight Tons", old, weight); // NOI18N
179        }
180    }
181
182    @Override
183    public String getWeightTons() {
184        String weight = null;
185        weight = engineModels.getModelWeight(getModel());
186        if (weight == null) {
187            weight = NONE;
188        }
189        return weight;
190    }
191
192    public void setBunit(boolean bUnit) {
193        if (getModel() == null || getModel().equals(NONE)) {
194            return;
195        }
196        boolean old = isBunit();
197        engineModels.setModelBunit(getModel(), bUnit);
198        if (old != bUnit) {
199            setDirtyAndFirePropertyChange(TYPE_CHANGED_PROPERTY, old, bUnit);
200        }
201    }
202
203    public boolean isBunit() {
204        return engineModels.isModelBunit(getModel());
205    }
206
207    /**
208     * Place locomotive in a consist
209     *
210     * @param consist The Consist to use.
211     *
212     */
213    public void setConsist(Consist consist) {
214        if (_consist == consist) {
215            return;
216        }
217        String old = "";
218        if (_consist != null) {
219            old = _consist.getName();
220            _consist.delete(this);
221        }
222        _consist = consist;
223        String newName = "";
224        if (_consist != null) {
225            _consist.add(this);
226            newName = _consist.getName();
227        }
228
229        if (!old.equals(newName)) {
230            setDirtyAndFirePropertyChange("consist", old, newName); // NOI18N
231        }
232    }
233
234    /**
235     * Get the consist for this locomotive
236     *
237     * @return null if locomotive isn't in a consist
238     */
239    public Consist getConsist() {
240        return _consist;
241    }
242
243    public String getConsistName() {
244        if (_consist != null) {
245            return _consist.getName();
246        }
247        return NONE;
248    }
249
250    /**
251     * B units that aren't part of a consist are blocked at the end.
252     */
253    @Override
254    public int getBlocking() {
255        if (isBunit() && getConsist() == null) {
256            return B_UNIT_BLOCKING;
257        }
258        return super.getBlocking();
259    }
260
261    /**
262     * Used to determine if engine is lead engine in a consist
263     *
264     * @return true if lead engine in a consist
265     */
266    public boolean isLead() {
267        if (getConsist() != null) {
268            return getConsist().isLead(this);
269        }
270        return false;
271    }
272
273    /**
274     * Get the DCC address for this engine from the JMRI roster. Does 4
275     * attempts, 1st by road and number, 2nd by number, 3rd by dccAddress using
276     * the engine's road number, 4th by id.
277     *
278     * @return dccAddress
279     */
280    public String getDccAddress() {
281        RosterEntry re = getRosterEntry();
282        if (re != null) {
283            return re.getDccAddress();
284        }
285        return NONE;
286    }
287
288    /**
289     * Get the RosterEntry for this engine from the JMRI roster. Does 4
290     * attempts, 1st by road and number, 2nd by number, 3rd by dccAddress using
291     * the engine's road number, 4th by id.
292     *
293     * @return RosterEntry, can be null
294     */
295    public RosterEntry getRosterEntry() {
296        RosterEntry rosterEntry = null;
297        // 1st by road name and number
298        List<RosterEntry> list =
299                Roster.getDefault().matchingList(getRoadName(), getNumber(), null, null, null, null, null);
300        if (list.size() > 0) {
301            rosterEntry = list.get(0);
302            log.debug("Roster Loco found by road and number: {}", rosterEntry.getDccAddress());
303            // 2nd by road number
304        } else if (!getNumber().equals(NONE)) {
305            list = Roster.getDefault().matchingList(null, getNumber(), null, null, null, null, null);
306            if (list.size() > 0) {
307                rosterEntry = list.get(0);
308                log.debug("Roster Loco found by number: {}", rosterEntry.getDccAddress());
309            }
310        }
311        // 3rd by dcc address
312        if (rosterEntry == null) {
313            list = Roster.getDefault().matchingList(null, null, getNumber(), null, null, null, null);
314            if (list.size() > 0) {
315                rosterEntry = list.get(0);
316                log.debug("Roster Loco found by dccAddress: {}", rosterEntry.getDccAddress());
317            }
318        }
319        // 4th by id
320        if (rosterEntry == null) {
321            list = Roster.getDefault().matchingList(null, null, null, null, null, null, getNumber());
322            if (list.size() > 0) {
323                rosterEntry = list.get(0);
324                log.debug("Roster Loco found by roster id: {}", rosterEntry.getDccAddress());
325            }
326        }
327        return rosterEntry;
328    }
329
330    /**
331     * Used to check destination track to see if it will accept locomotive
332     *
333     * @return status, see RollingStock.java
334     */
335    @Override
336    public String checkDestination(Location destination, Track track) {
337        return super.checkDestination(destination, track);
338    }
339    
340    @Override
341    public String setDestination(Location destination, Track track, boolean force) {
342        String destinationName = getDestinationName();
343        String status = super.setDestination(destination, track, force);
344        // return if not Okay
345        if (!status.equals(Track.OKAY)) {
346            return status;
347        }
348        if (destinationName.equals(NONE) || (destination != null) || getTrain() == null) {
349            return status;
350        }
351        // engine clone was in a train and has been dropped off
352        if (isClone()) {
353            // destroy clone
354            InstanceManager.getDefault(ConsistManager.class).deleteConsist(getConsistName());
355            InstanceManager.getDefault(EngineManager.class).deregister(this);
356        }
357        return status;
358    }
359
360    /**
361     * Determine if there's a change in the lead locomotive. There are two
362     * possible locations in a train's route. TODO this code places the last
363     * loco added to the train as the lead. It would be better if the first one
364     * became the lead loco.
365     */
366    @Override
367    protected void moveRollingStock(RouteLocation current, RouteLocation next) {
368        if (current == getRouteLocation()) {
369            if (getConsist() == null || isLead()) {
370                if (getRouteLocation() != getRouteDestination() &&
371                        getTrain() != null &&
372                        !isBunit() &&
373                        getTrain().getLeadEngine() != this) {
374                    if (((getTrain().getSecondLegStartRouteLocation() == current &&
375                            (getTrain().getSecondLegOptions() & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES)) ||
376                            ((getTrain().getThirdLegStartRouteLocation() == current &&
377                                    (getTrain().getThirdLegOptions() &
378                                            Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES))) {
379                        log.debug("New lead locomotive ({}) for train ({})", toString(), getTrain().getName());
380                        getTrain().setLeadEngine(this);
381                        getTrain().createTrainIcon(current);
382                    }
383                }
384            }
385        }
386        super.moveRollingStock(current, next);
387    }
388    
389    @Override
390    public void reset() {
391        super.reset();
392        destroyClone();
393    }
394    
395    /*
396     * This routine destroys the clone and restores the cloned car to its
397     * original location and load. Note there can be multiple clones for a car.
398     * Only the first clone created has the right info. A clone has creation
399     * order number appended to the road number.
400     */
401    private void destroyClone() {
402        if (isClone()) {
403            // move cloned engine back to original location
404            EngineManager engineManager = InstanceManager.getDefault(EngineManager.class);
405            String[] number = getNumber().split(Engine.CLONE_REGEX);
406            Engine engine = engineManager.getByRoadAndNumber(getRoadName(), number[0]);
407            if (engine != null) {
408                int cloneCreationNumber = Integer.parseInt(number[1]);
409                if (cloneCreationNumber <= engine.getCloneOrder()) {
410                    destroyCloneReset(engine);
411                    // remember the last clone destroyed
412                    engine.setCloneOrder(cloneCreationNumber);
413                }
414            } else {
415                log.error("Not able to find and restore loco ({}, {})", getRoadName(), number[0]);
416            }
417            InstanceManager.getDefault(ConsistManager.class).deleteConsist(getConsistName());
418            engineManager.deregister(this);
419        }
420    }
421
422    @Override
423    public void dispose() {
424        setConsist(null);
425        super.dispose();
426    }
427
428    /**
429     * Construct this Entry from XML. This member has to remain synchronized
430     * with the detailed DTD in operations-engines.dtd
431     *
432     * @param e Engine XML element
433     */
434    public Engine(org.jdom2.Element e) {
435        super(e); // MUST create the rolling stock first!
436        org.jdom2.Attribute a;
437        // must set _model first so locomotive hp, length, type and weight is set properly
438        if ((a = e.getAttribute(Xml.MODEL)) != null) {
439            _model = a.getValue();
440        }
441        if ((a = e.getAttribute(Xml.HP)) != null) {
442            setHp(a.getValue());
443        }
444        if ((a = e.getAttribute(Xml.LENGTH)) != null) {
445            setLength(a.getValue());
446        }
447        if ((a = e.getAttribute(Xml.TYPE)) != null) {
448            setTypeName(a.getValue());
449        }
450        if ((a = e.getAttribute(Xml.WEIGHT_TONS)) != null) {
451            setWeightTons(a.getValue());
452        }
453        if ((a = e.getAttribute(Xml.B_UNIT)) != null) {
454            setBunit(a.getValue().equals(Xml.TRUE));
455        }
456        if ((a = e.getAttribute(Xml.CONSIST)) != null) {
457            Consist c = InstanceManager.getDefault(ConsistManager.class).getConsistByName(a.getValue());
458            if (c != null) {
459                setConsist(c);
460                if ((a = e.getAttribute(Xml.LEAD_CONSIST)) != null && a.getValue().equals(Xml.TRUE)) {
461                    _consist.setLead(this);
462                }
463                if ((a = e.getAttribute(Xml.CONSIST_NUM)) != null) {
464                    _consist.setConsistNumber(Integer.parseInt(a.getValue()));
465                }
466            } else {
467                log.error("Consist {} does not exist", a.getValue());
468            }
469        }
470    }
471
472    boolean verboseStore = false;
473
474    /**
475     * Create an XML element to represent this Entry. This member has to remain
476     * synchronized with the detailed DTD in operations-engines.dtd.
477     *
478     * @return Contents in a JDOM Element
479     */
480    public org.jdom2.Element store() {
481        org.jdom2.Element e = new org.jdom2.Element(Xml.ENGINE);
482        super.store(e);
483        e.setAttribute(Xml.MODEL, getModel());
484        e.setAttribute(Xml.HP, getHp());
485        e.setAttribute(Xml.B_UNIT, (isBunit() ? Xml.TRUE : Xml.FALSE));
486        if (getConsist() != null) {
487            e.setAttribute(Xml.CONSIST, getConsistName());
488            if (isLead()) {
489                e.setAttribute(Xml.LEAD_CONSIST, Xml.TRUE);
490                if (getConsist().getConsistNumber() > 0) {
491                    e.setAttribute(Xml.CONSIST_NUM,
492                            Integer.toString(getConsist().getConsistNumber()));
493                }
494            }
495        }
496        return e;
497    }
498
499    @Override
500    protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
501        // Set dirty
502        InstanceManager.getDefault(EngineManagerXml.class).setDirty(true);
503        super.setDirtyAndFirePropertyChange(p, old, n);
504    }
505
506    @Override
507    protected void addPropertyChangeListeners() {
508        InstanceManager.getDefault(EngineTypes.class).addPropertyChangeListener(this);
509        InstanceManager.getDefault(EngineLengths.class).addPropertyChangeListener(this);
510        super.addPropertyChangeListeners();
511    }
512    
513    @Override
514    protected void removePropertyChangeListeners() {
515        InstanceManager.getDefault(EngineTypes.class).removePropertyChangeListener(this);
516        InstanceManager.getDefault(EngineLengths.class).removePropertyChangeListener(this);
517        super.removePropertyChangeListeners();
518    }
519
520    @Override
521    public void propertyChange(PropertyChangeEvent e) {
522        super.propertyChange(e);
523        if (e.getPropertyName().equals(EngineTypes.ENGINETYPES_NAME_CHANGED_PROPERTY)) {
524            if (e.getOldValue().equals(getTypeName())) {
525                log.debug("Loco ({}) sees type name change old: ({}) new: ({})", toString(),
526                            e.getOldValue(), e.getNewValue()); // NOI18N
527                setTypeName((String) e.getNewValue());
528            }
529        }
530        if (e.getPropertyName().equals(EngineLengths.ENGINELENGTHS_NAME_CHANGED_PROPERTY)) {
531            if (e.getOldValue().equals(getLength())) {
532                log.debug("Loco ({}) sees length name change old: {} new: {}", toString(), e.getOldValue(), e
533                        .getNewValue()); // NOI18N
534                setLength((String) e.getNewValue());
535            }
536        }
537    }
538
539    private static final Logger log = LoggerFactory.getLogger(Engine.class);
540
541}