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}