001package jmri.managers; 002 003import java.beans.PropertyChangeEvent; 004import java.beans.PropertyChangeListener; 005 006import java.time.LocalDateTime; 007import java.time.temporal.ChronoUnit; 008import java.util.Objects; 009import javax.annotation.CheckForNull; 010import javax.annotation.Nonnull; 011import javax.annotation.OverridingMethodsMustInvokeSuper; 012 013import jmri.*; 014import jmri.implementation.SignalSpeedMap; 015import jmri.SystemConnectionMemo; 016import org.slf4j.Logger; 017import org.slf4j.LoggerFactory; 018 019/** 020 * Abstract partial implementation of a TurnoutManager. 021 * 022 * @author Bob Jacobsen Copyright (C) 2001 023 */ 024public abstract class AbstractTurnoutManager extends AbstractManager<Turnout> 025 implements TurnoutManager { 026 027 public AbstractTurnoutManager(SystemConnectionMemo memo) { 028 super(memo); 029 InstanceManager.getDefault(TurnoutOperationManager.class); // force creation of an instance 030 init(); 031 } 032 033 final void init() { 034 InstanceManager.getDefault(SensorManager.class).addVetoableChangeListener(this); 035 // set listener for changes in memo 036 memo.addPropertyChangeListener(pcl); 037 } 038 039 final PropertyChangeListener pcl = (PropertyChangeEvent e) -> { 040 if (e.getPropertyName().equals(SystemConnectionMemo.INTERVAL)) { 041 handleIntervalChange((Integer) e.getNewValue()); 042 } 043 }; 044 045 /** {@inheritDoc} */ 046 @Override 047 public int getXMLOrder() { 048 return Manager.TURNOUTS; 049 } 050 051 /** {@inheritDoc} */ 052 @Override 053 public char typeLetter() { 054 return 'T'; 055 } 056 057 /** {@inheritDoc} */ 058 @Override 059 @Nonnull 060 public Turnout provideTurnout(@Nonnull String name) { 061 log.debug("provide turnout {}", name); 062 Turnout result = getTurnout(name); 063 return result == null ? newTurnout(makeSystemName(name, true), null) : result; 064 } 065 066 /** {@inheritDoc} */ 067 @Override 068 @CheckForNull 069 public Turnout getTurnout(@Nonnull String name) { 070 Turnout result = getByUserName(name); 071 if (result == null) { 072 result = getBySystemName(name); 073 } 074 return result; 075 } 076 077 /** {@inheritDoc} */ 078 @Override 079 @Nonnull 080 public Turnout newTurnout(@Nonnull String systemName, @CheckForNull String userName) throws IllegalArgumentException { 081 Objects.requireNonNull(systemName, "SystemName cannot be null. UserName was " + ((userName == null) ? "null" : userName)); // NOI18N 082 // add normalize? see AbstractSensor 083 log.debug("newTurnout: {};{}", systemName, userName); 084 085 // is system name in correct format? 086 if (!systemName.startsWith(getSystemNamePrefix()) 087 || !(systemName.length() > (getSystemNamePrefix()).length())) { 088 log.error("Invalid system name for Turnout: {} needed {}{} followed by a suffix", 089 systemName, getSystemPrefix(), typeLetter()); 090 throw new IllegalArgumentException("Invalid system name for Turnout: " + systemName 091 + " needed " + getSystemNamePrefix()); 092 } 093 094 // return existing if there is one 095 Turnout t; 096 if (userName != null) { 097 t = getByUserName(userName); 098 if (t != null) { 099 if (getBySystemName(systemName) != t) { 100 log.error("inconsistent user ({}) and system name ({}) results; userName related to ({})", 101 userName, systemName, t.getSystemName()); 102 } 103 return t; 104 } 105 } 106 t = getBySystemName(systemName); 107 if (t != null) { 108 if ((t.getUserName() == null) && (userName != null)) { 109 t.setUserName(userName); 110 } else if (userName != null) { 111 log.warn("Found turnout via system name ({}) with non-null user name ({}). Turnout \"{} ({})\" cannot be used.", 112 systemName, t.getUserName(), systemName, userName); 113 } 114 return t; 115 } 116 117 // doesn't exist, make a new one 118 t = createNewTurnout(systemName, userName); 119 // if that failed, will throw an IllegalArgumentException 120 121 // Some implementations of createNewTurnout() register the new bean, 122 // some don't. 123 if (getBySystemName(t.getSystemName()) == null) { 124 // save in the maps if successful 125 register(t); 126 } 127 128 try { 129 t.setStraightSpeed("Global"); 130 } catch (jmri.JmriException ex) { 131 log.error("Turnout : {} : {}", t, ex.getMessage()); 132 } 133 134 try { 135 t.setDivergingSpeed("Global"); 136 } catch (jmri.JmriException ex) { 137 log.error("Turnout : {} : {}", t, ex.getMessage()); 138 } 139 return t; 140 } 141 142 /** {@inheritDoc} */ 143 @Override 144 @Nonnull 145 public String getBeanTypeHandled(boolean plural) { 146 return Bundle.getMessage(plural ? "BeanNameTurnouts" : "BeanNameTurnout"); 147 } 148 149 /** 150 * {@inheritDoc} 151 */ 152 @Override 153 public Class<Turnout> getNamedBeanClass() { 154 return Turnout.class; 155 } 156 157 /** {@inheritDoc} */ 158 @Override 159 @Nonnull 160 public String getClosedText() { 161 return Bundle.getMessage("TurnoutStateClosed"); 162 } 163 164 /** {@inheritDoc} */ 165 @Override 166 @Nonnull 167 public String getThrownText() { 168 return Bundle.getMessage("TurnoutStateThrown"); 169 } 170 171 /** 172 * Get from the user, the number of addressed bits used to control a 173 * turnout. Normally this is 1, and the default routine returns 1 174 * automatically. Turnout Managers for systems that can handle multiple 175 * control bits should override this method with one which asks the user to 176 * specify the number of control bits. If the user specifies more than one 177 * control bit, this method should check if the additional bits are 178 * available (not assigned to another object). If the bits are not 179 * available, this method should return 0 for number of control bits, after 180 * informing the user of the problem. 181 */ 182 @Override 183 public int askNumControlBits(@Nonnull String systemName) { 184 return 1; 185 } 186 187 /** {@inheritDoc} */ 188 @Override 189 public boolean isNumControlBitsSupported(@Nonnull String systemName) { 190 return false; 191 } 192 193 /** 194 * Get from the user, the type of output to be used bits to control a 195 * turnout. Normally this is 0 for 'steady state' control, and the default 196 * routine returns 0 automatically. Turnout Managers for systems that can 197 * handle pulsed control as well as steady state control should override 198 * this method with one which asks the user to specify the type of control 199 * to be used. The routine should return 0 for 'steady state' control, or n 200 * for 'pulsed' control, where n specifies the duration of the pulse 201 * (normally in seconds). 202 */ 203 @Override 204 public int askControlType(@Nonnull String systemName) { 205 return 0; 206 } 207 208 /** {@inheritDoc} */ 209 @Override 210 public boolean isControlTypeSupported(@Nonnull String systemName) { 211 return false; 212 } 213 214 /** 215 * Internal method to invoke the factory, after all the logic for returning 216 * an existing Turnout has been invoked. 217 * 218 * @param systemName the system name to use for the new Turnout 219 * @param userName the user name to use for the new Turnout 220 * @return the new Turnout or 221 * @throws IllegalArgumentException if unsuccessful 222 */ 223 @Nonnull 224 abstract protected Turnout createNewTurnout(@Nonnull String systemName, String userName) throws IllegalArgumentException; 225 226 /** {@inheritDoc} */ 227 @Override 228 @Nonnull 229 public String[] getValidOperationTypes() { 230 if (jmri.InstanceManager.getNullableDefault(jmri.CommandStation.class) != null) { 231 return new String[]{"Sensor", "Raw", "NoFeedback"}; 232 } else { 233 return new String[]{"Sensor", "NoFeedback"}; 234 } 235 } 236 237 /** 238 * Default Turnout ensures a numeric only system name. 239 * {@inheritDoc} 240 */ 241 @Nonnull 242 @Override 243 public String createSystemName(@Nonnull String curAddress, @Nonnull String prefix) throws JmriException { 244 return prefix + typeLetter() + checkNumeric(curAddress); 245 } 246 247 private String defaultClosedSpeed = "Normal"; 248 private String defaultThrownSpeed = "Restricted"; 249 250 /** {@inheritDoc} */ 251 @Override 252 public void setDefaultClosedSpeed(@Nonnull String speed) throws JmriException { 253 Objects.requireNonNull(speed, "Value of requested turnout default closed speed can not be null"); 254 255 if (defaultClosedSpeed.equals(speed)) { 256 return; 257 } 258 if (speed.contains("Block")) { 259 speed = "Block"; 260 if (defaultClosedSpeed.equals(speed)) { 261 return; 262 } 263 } else { 264 try { 265 Float.parseFloat(speed); 266 } catch (NumberFormatException nx) { 267 try { 268 jmri.InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(speed); 269 } catch (IllegalArgumentException ex) { 270 throw new JmriException("Value of requested turnout default closed speed is not valid. " + ex.getMessage()); 271 } 272 } 273 } 274 String oldSpeed = defaultClosedSpeed; 275 defaultClosedSpeed = speed; 276 firePropertyChange("DefaultTurnoutClosedSpeedChange", oldSpeed, speed); 277 } 278 279 /** {@inheritDoc} */ 280 @Override 281 public void setDefaultThrownSpeed(@Nonnull String speed) throws JmriException { 282 Objects.requireNonNull(speed, "Value of requested turnout default thrown speed can not be null"); 283 284 if (defaultThrownSpeed.equals(speed)) { 285 return; 286 } 287 if (speed.contains("Block")) { 288 speed = "Block"; 289 if (defaultThrownSpeed.equals(speed)) { 290 return; 291 } 292 293 } else { 294 try { 295 Float.parseFloat(speed); 296 } catch (NumberFormatException nx) { 297 try { 298 jmri.InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(speed); 299 } catch (IllegalArgumentException ex) { 300 throw new JmriException("Value of requested turnout default thrown speed is not valid. " + ex.getMessage()); 301 } 302 } 303 } 304 String oldSpeed = defaultThrownSpeed; 305 defaultThrownSpeed = speed; 306 firePropertyChange("DefaultTurnoutThrownSpeedChange", oldSpeed, speed); 307 } 308 309 /** {@inheritDoc} */ 310 @Override 311 public String getDefaultThrownSpeed() { 312 return defaultThrownSpeed; 313 } 314 315 /** {@inheritDoc} */ 316 @Override 317 public String getDefaultClosedSpeed() { 318 return defaultClosedSpeed; 319 } 320 321 /** {@inheritDoc} */ 322 @Override 323 public String getEntryToolTip() { 324 return Bundle.getMessage("EnterNumber1to9999ToolTip"); 325 } 326 327 private void handleIntervalChange(int newVal) { 328 turnoutInterval = newVal; 329 log.debug("in memo turnoutInterval changed to {}", turnoutInterval); 330 } 331 332 /** {@inheritDoc} */ 333 @Override 334 public int getOutputInterval() { 335 return turnoutInterval; 336 } 337 338 /** {@inheritDoc} */ 339 @Override 340 public void setOutputInterval(int newInterval) { 341 memo.setOutputInterval(newInterval); 342 turnoutInterval = newInterval; // local field will hear change and update automatically? 343 log.debug("turnoutInterval set to: {}", newInterval); 344 } 345 346 /** 347 * Duration in milliseconds of interval between separate Turnout commands on the same connection. 348 * <p> 349 * Change from e.g. XNetTurnout extensions and scripts using {@link #setOutputInterval(int)} 350 */ 351 private int turnoutInterval = memo.getOutputInterval(); 352 private LocalDateTime waitUntil = LocalDateTime.now(); 353 354 /** {@inheritDoc} */ 355 @Override 356 @Nonnull 357 public LocalDateTime outputIntervalEnds() { 358 log.debug("outputIntervalEnds called in AbstractTurnoutManager"); 359 if (waitUntil.isAfter(LocalDateTime.now())) { 360 waitUntil = waitUntil.plus(turnoutInterval, ChronoUnit.MILLIS); 361 } else { 362 waitUntil = LocalDateTime.now().plus(turnoutInterval, ChronoUnit.MILLIS); // default interval = 250 Msec 363 } 364 return waitUntil; 365 } 366 367 /** 368 * Removes SensorManager and SystemConnectionMemo change listeners. 369 * {@inheritDoc} 370 */ 371 @OverridingMethodsMustInvokeSuper 372 @Override 373 public void dispose(){ 374 memo.removePropertyChangeListener(pcl); 375 InstanceManager.getDefault(SensorManager.class).removeVetoableChangeListener(this); 376 super.dispose(); 377 } 378 379 private final static Logger log = LoggerFactory.getLogger(AbstractTurnoutManager.class); 380 381}