001package jmri.jmrit.timetable;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyVetoException;
005import java.beans.VetoableChangeListener;
006import jmri.Scale;
007import jmri.ScaleManager;
008import jmri.jmrit.timetable.swing.TimeTableFrame;
009
010/**
011 * Define the content of a Layout record.
012 * <p>
013 * The fast clock, scale and metric values affect the scale mile / scale km.
014 * When these are changed, the stop times for all of the trains have to be
015 * re-calculated.  Depending on the schedule limits, this can result in
016 * calculation errors.  When this occurs, exceptions occur which trigger
017 * rolling back the changes.
018 * @author Dave Sand Copyright (C) 2018
019 */
020public class Layout implements VetoableChangeListener {
021
022    public static final String SCALE_RATIO = "ScaleRatio";
023
024    /**
025     * Create a new layout with default values.
026     */
027    public Layout() {
028        _layoutId = _dm.getNextId("Layout");  // NOI18N
029        _dm.addLayout(_layoutId, this);
030        _scale.addVetoableChangeListener(SCALE_RATIO, this);  // NOI18N
031        setScaleMK();
032    }
033
034    public Layout(int layoutId, String layoutName, Scale scale, int fastClock, int throttles, boolean metric) {
035        _layoutId = layoutId;
036        setLayoutName(layoutName);
037        setScale(scale);
038        setFastClock(fastClock);
039        setThrottles(throttles);
040        setMetric(metric);
041    }
042
043    TimeTableDataManager _dm = TimeTableDataManager.getDataManager();
044
045    private final int _layoutId;
046    private String _layoutName = Bundle.getMessage("NewLayoutName");  // NOI18N
047    private Scale _scale = ScaleManager.getScale("HO");  // NOI18N
048    private int _fastClock = 4;
049    private int _throttles = 0;
050    private boolean _metric = false;
051
052    private double _ratio = 87.1;
053    private double _scaleMK;          // Scale mile (real feet) or km (real meter)
054
055    /**
056     * Make a copy of the layout.
057     * @return a new layout instance.
058     */
059    public Layout getCopy() {
060        Layout copy = new Layout();
061        copy.setLayoutName(Bundle.getMessage("DuplicateCopyName", _layoutName));
062        copy.setScale(_scale);
063        copy.setFastClock(_fastClock);
064        copy.setThrottles(_throttles);
065        copy.setMetric(_metric);
066        return copy;
067    }
068
069    /**
070     * Calculate the length of a scale mile or scale kilometer.
071     * The values are adjusted for scale and fast clock ratio.
072     * The resulting value is the real feet or meters.
073     * The final step is to re-calculate the train times.
074     * @throws IllegalArgumentException The calculate can throw an exception which will get re-thrown.
075     */
076    public void setScaleMK() {
077        double distance = (_metric) ? 1000 : 5280;
078        _scaleMK = distance / _ratio / _fastClock;
079        log.debug("scaleMK = {}, scale = {}", _scaleMK, _scale);  // NOI18N
080
081        _dm.calculateLayoutTrains(getLayoutId(), false);
082        _dm.calculateLayoutTrains(getLayoutId(), true);
083    }
084
085    public double getScaleMK() {
086        return _scaleMK;
087    }
088
089    public int getLayoutId() {
090        return _layoutId;
091    }
092
093    public String getLayoutName() {
094        return _layoutName;
095    }
096
097    public void setLayoutName(String newName) {
098        _layoutName = newName;
099    }
100
101    public double getRatio() {
102        return _ratio;
103    }
104
105    public Scale getScale() {
106        return _scale;
107    }
108
109    public void setScale(Scale newScale) {
110        _scale.removeVetoableChangeListener(SCALE_RATIO, this);  // NOI18N
111        if (newScale == null) {
112            newScale = ScaleManager.getScale("HO");  // NOI18N
113            log.warn("No scale found, defaulting to HO");  // NOI18N
114        }
115
116        Scale oldScale = _scale;
117        double oldRatio = _ratio;
118        _scale = newScale;
119        _ratio = newScale.getScaleRatio();
120
121        try {
122            // Update the smile/skm which includes stop recalcs
123            setScaleMK();
124        } catch (IllegalArgumentException ex) {
125            _scale = oldScale;  // roll back scale and ratio
126            _ratio = oldRatio;
127            setScaleMK();
128            throw ex;
129        }
130        _scale.addVetoableChangeListener(SCALE_RATIO, this);  // NOI18N
131    }
132
133    public int getFastClock() {
134        return _fastClock;
135    }
136
137    /**
138     * Set a new fast clock speed, update smile/skm.
139     * @param newClock The value to be used.
140     * @throws IllegalArgumentException (CLOCK_LT_1) if the value is less than 1.
141     * will also re-throw a recalc error.
142     */
143    public void setFastClock(int newClock) {
144        if (newClock < 1) {
145            throw new IllegalArgumentException(TimeTableDataManager.CLOCK_LT_1);
146        }
147        int oldClock = _fastClock;
148        _fastClock = newClock;
149
150        try {
151            // Update the smile/skm which includes stop recalcs
152            setScaleMK();
153        } catch (IllegalArgumentException ex) {
154            _fastClock = oldClock;  // roll back
155            setScaleMK();
156            throw ex;
157        }
158    }
159
160    public int getThrottles() {
161        return _throttles;
162    }
163
164    /**
165     * Set the new value for throttles.
166     * @param newThrottles The new throttle count.
167     * @throws IllegalArgumentException (THROTTLES_USED, THROTTLES_LT_0) when the
168     * new count is less than train references or a negative number was passed.
169     */
170    public void setThrottles(int newThrottles) {
171        if (newThrottles < 0) {
172            throw new IllegalArgumentException(TimeTableDataManager.THROTTLES_LT_0);
173        }
174        for (Schedule schedule : _dm.getSchedules(_layoutId, true)) {
175            for (Train train : _dm.getTrains(schedule.getScheduleId(), 0, true)) {
176                if (train.getThrottle() > newThrottles) {
177                    throw new IllegalArgumentException(TimeTableDataManager.THROTTLES_IN_USE);
178                }
179            }
180        }
181        _throttles = newThrottles;
182    }
183
184    public boolean getMetric() {
185        return _metric;
186    }
187
188    /**
189     * Set metric flag, update smile/skm.
190     * @param newMetric True for metric units.
191     * @throws IllegalArgumentException if there was a recalc error.
192     */
193    public void setMetric(boolean newMetric) {
194        boolean oldMetric = _metric;
195        _metric = newMetric;
196
197        try {
198            // Update the smile/skm which includes stop recalcs
199            setScaleMK();
200        } catch (IllegalArgumentException ex) {
201            _metric = oldMetric;  // roll back
202            setScaleMK();
203            throw ex;
204        }
205    }
206
207    @Override
208    public String toString() {
209        return _layoutName;
210    }
211
212    /**
213     * Listen for ratio changes to my current scale.  Verify that the new ratio
214     * is neither too small nor too large.  Too large can cause train times to move
215     * outside of the schedule window.  If the new ratio is invalid, the change
216     * will be vetoed.
217     * @param evt The scale ratio property change event.
218     * @throws PropertyVetoException The message will depend on the actual error.
219     */
220    @Override
221    public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException {
222        log.debug("scale change event: layout = {}, evt = {}", _layoutName, evt);
223        double newRatio = (Double) evt.getNewValue();
224        if (newRatio < 1.0) {
225            throw new PropertyVetoException("Ratio is less than 1", evt);
226        }
227
228        double oldRatio = _ratio;
229        _ratio = newRatio;
230
231        try {
232            // Update the smile/skm which includes stop recalcs
233            setScaleMK();
234        } catch (IllegalArgumentException ex) {
235            // Roll back the ratio change
236            _ratio = oldRatio;
237            setScaleMK();
238            throw new PropertyVetoException("New ratio causes calc errors", evt);
239        }
240
241        TimeTableFrame frame = jmri.InstanceManager.getNullableDefault(TimeTableFrame.class);
242        if (frame != null) {
243            frame.setShowReminder(true);
244        } else {
245            // Save any changes
246            jmri.jmrit.timetable.configurexml.TimeTableXml.doStore();
247        }
248    }
249
250    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Layout.class);
251}