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