001package jmri.jmrit.logixng.util; 002 003import java.beans.PropertyChangeEvent; 004import java.beans.PropertyChangeListener; 005import java.beans.PropertyVetoException; 006import java.beans.VetoableChangeListener; 007import java.util.HashMap; 008import java.util.Locale; 009import java.util.Map; 010 011import javax.annotation.Nonnull; 012 013import jmri.*; 014import jmri.jmrit.logixng.*; 015import jmri.jmrit.logixng.implementation.AbstractBase; 016import jmri.jmrit.logixng.util.parser.*; 017import jmri.jmrit.logixng.util.parser.RecursiveDescentParser; 018import jmri.util.TypeConversionUtil; 019 020/** 021 * Select a double for LogixNG actions and expressions. 022 * 023 * @author Daniel Bergqvist (C) 2022 024 */ 025public class LogixNG_SelectDouble implements VetoableChangeListener { 026 027 private final AbstractBase _base; 028 private final InUse _inUse; 029 private final LogixNG_SelectTable _selectTable; 030 private final int _numDecimals; 031 private final PropertyChangeListener _listener; 032 private boolean _listenToMemory; 033 private boolean _listenersAreRegistered; 034 private final FormatterParserValidator _formatterParserValidator; 035 036 private NamedBeanAddressing _addressing = NamedBeanAddressing.Direct; 037 private double _value; 038 private String _reference = ""; 039 private String _localVariable = ""; 040 private NamedBeanHandle<Memory> _memoryHandle; 041 private String _formula = ""; 042 private ExpressionNode _expressionNode; 043 044 045 public LogixNG_SelectDouble( 046 @Nonnull AbstractBase base, 047 int numDecimals, 048 @Nonnull PropertyChangeListener listener) { 049 050 this(base, numDecimals, listener, new DefaultFormatterParserValidator()); 051 } 052 053 public LogixNG_SelectDouble( 054 @Nonnull AbstractBase base, 055 int numDecimals, 056 @Nonnull PropertyChangeListener listener, 057 @Nonnull FormatterParserValidator formatterParserValidator) { 058 _base = base; 059 _inUse = () -> true; 060 _selectTable = new LogixNG_SelectTable(_base, _inUse); 061 _numDecimals = numDecimals; 062 _listener = listener; 063 _formatterParserValidator = formatterParserValidator; 064 _value = _formatterParserValidator.getInitialValue(); 065 } 066 067 public void copy(LogixNG_SelectDouble copy) throws ParserException { 068 copy.setAddressing(_addressing); 069 copy.setValue(_value); 070 copy.setLocalVariable(_localVariable); 071 copy.setMemory(_memoryHandle); 072 copy.setListenToMemory(_listenToMemory); 073 copy.setReference(_reference); 074 copy.setFormula(_formula); 075 _selectTable.copy(copy._selectTable); 076 } 077 078 @Nonnull 079 public FormatterParserValidator getFormatterParserValidator() { 080 return _formatterParserValidator; 081 } 082 083 public void setAddressing(@Nonnull NamedBeanAddressing addressing) throws ParserException { 084 this._addressing = addressing; 085 parseFormula(); 086 } 087 088 public boolean isDirectAddressing() { 089 return _addressing == NamedBeanAddressing.Direct; 090 } 091 092 public NamedBeanAddressing getAddressing() { 093 return _addressing; 094 } 095 096 public void setValue(double value) { 097 _base.assertListenersAreNotRegistered(log, "setEnum"); 098 _value = value; 099 } 100 101 public double getValue() { 102 return _value; 103 } 104 105 public void setReference(@Nonnull String reference) { 106 if ((! reference.isEmpty()) && (! ReferenceUtil.isReference(reference))) { 107 throw new IllegalArgumentException("The reference \"" + reference + "\" is not a valid reference"); 108 } 109 _reference = reference; 110 } 111 112 public String getReference() { 113 return _reference; 114 } 115 116 public void setLocalVariable(@Nonnull String localVariable) { 117 _localVariable = localVariable; 118 } 119 120 public String getLocalVariable() { 121 return _localVariable; 122 } 123 124 public void setMemory(@Nonnull String memoryName) { 125 Memory memory = InstanceManager.getDefault(MemoryManager.class).getMemory(memoryName); 126 if (memory != null) { 127 setMemory(memory); 128 } else { 129 removeMemory(); 130 log.warn("memory \"{}\" is not found", memoryName); 131 } 132 } 133 134 public void setMemory(@Nonnull NamedBeanHandle<Memory> handle) { 135 _memoryHandle = handle; 136 InstanceManager.memoryManagerInstance().addVetoableChangeListener(this); 137 addRemoveVetoListener(); 138 } 139 140 public void setMemory(@Nonnull Memory memory) { 141 setMemory(InstanceManager.getDefault(NamedBeanHandleManager.class) 142 .getNamedBeanHandle(memory.getDisplayName(), memory)); 143 } 144 145 public void removeMemory() { 146 if (_memoryHandle != null) { 147 _memoryHandle = null; 148 addRemoveVetoListener(); 149 } 150 } 151 152 public NamedBeanHandle<Memory> getMemory() { 153 return _memoryHandle; 154 } 155 156 public void setListenToMemory(boolean listenToMemory) { 157 _listenToMemory = listenToMemory; 158 } 159 160 public boolean getListenToMemory() { 161 return _listenToMemory; 162 } 163 164 public void setFormula(@Nonnull String formula) throws ParserException { 165 _formula = formula; 166 parseFormula(); 167 } 168 169 public String getFormula() { 170 return _formula; 171 } 172 173 private void parseFormula() throws ParserException { 174 if (_addressing == NamedBeanAddressing.Formula) { 175 Map<String, Variable> variables = new HashMap<>(); 176 177 RecursiveDescentParser parser = new RecursiveDescentParser(variables); 178 _expressionNode = parser.parseExpression(_formula); 179 } else { 180 _expressionNode = null; 181 } 182 } 183 184 public LogixNG_SelectTable getSelectTable() { 185 return _selectTable; 186 } 187 188 private void addRemoveVetoListener() { 189 if (_memoryHandle != null) { 190 InstanceManager.getDefault(MemoryManager.class).addVetoableChangeListener(this); 191 } else { 192 InstanceManager.getDefault(MemoryManager.class).removeVetoableChangeListener(this); 193 } 194 } 195 196 public double evaluateValue(ConditionalNG conditionalNG) throws JmriException { 197 198 if (_addressing == NamedBeanAddressing.Direct) { 199 return _value; 200 } else { 201 Object val; 202 203 SymbolTable symbolNamedBean = conditionalNG.getSymbolTable(); 204 205 switch (_addressing) { 206 case Reference: 207 val = ReferenceUtil.getReference( 208 conditionalNG.getSymbolTable(), _reference); 209 break; 210 211 case LocalVariable: 212 val = symbolNamedBean.getValue(_localVariable); 213 break; 214 215 case Memory: 216 val = _memoryHandle.getBean().getValue(); 217 break; 218 219 case Formula: 220 val = _expressionNode != null 221 ? _expressionNode.calculate(conditionalNG.getSymbolTable()) 222 : null; 223 break; 224 225 case Table: 226 val = _selectTable.evaluateTableData(conditionalNG); 227 break; 228 229 default: 230 throw new IllegalArgumentException("invalid _addressing state: " + _addressing.name()); 231 } 232 233 if (val instanceof String) { 234 String validateResult = _formatterParserValidator.validate(val.toString()); 235 if (validateResult != null) throw new JmriException(validateResult); 236 return _formatterParserValidator.parse(val.toString()); 237 } 238 239 return TypeConversionUtil.convertToDouble(val, false, true, true); 240 } 241 } 242 243 /** 244 * Format the value 245 * @param value The value 246 * @return speed formatted as %1.3f 247 */ 248 public String formatValue(double value) { 249 return String.format(String.format("%%1.%df", _numDecimals), value); 250 } 251 252 public String getDescription(Locale locale) { 253 String enumName; 254 255 String memoryName; 256 if (_memoryHandle != null) { 257 memoryName = _memoryHandle.getName(); 258 } else { 259 memoryName = Bundle.getMessage(locale, "BeanNotSelected"); 260 } 261 262 switch (_addressing) { 263 case Direct: 264 enumName = Bundle.getMessage(locale, "AddressByDirect", _value); 265 break; 266 267 case Reference: 268 enumName = Bundle.getMessage(locale, "AddressByReference", _reference); 269 break; 270 271 case Memory: 272 enumName = Bundle.getMessage(locale, "AddressByMemory_Listen", memoryName, Base.getListenString(_listenToMemory)); 273 break; 274 275 case LocalVariable: 276 enumName = Bundle.getMessage(locale, "AddressByLocalVariable", _localVariable); 277 break; 278 279 case Formula: 280 enumName = Bundle.getMessage(locale, "AddressByFormula", _formula); 281 break; 282 283 case Table: 284 enumName = Bundle.getMessage( 285 locale, 286 "AddressByTable", 287 _selectTable.getTableNameDescription(locale), 288 _selectTable.getTableRowDescription(locale), 289 _selectTable.getTableColumnDescription(locale)); 290 break; 291 292 default: 293 throw new IllegalArgumentException("invalid _addressing: " + _addressing.name()); 294 } 295 return enumName; 296 } 297 298 /** 299 * Register listeners if this object needs that. 300 */ 301 public void registerListeners() { 302 if (!_listenersAreRegistered 303 && (_addressing == NamedBeanAddressing.Memory) 304 && (_memoryHandle != null) 305 && _listenToMemory) { 306 _memoryHandle.getBean().addPropertyChangeListener("value", _listener); 307 _listenersAreRegistered = true; 308 } 309 } 310 311 /** 312 * Unregister listeners if this object needs that. 313 */ 314 public void unregisterListeners() { 315 if (_listenersAreRegistered 316 && (_addressing == NamedBeanAddressing.Memory) 317 && (_memoryHandle != null) 318 && _listenToMemory) { 319 _memoryHandle.getBean().removePropertyChangeListener("value", _listener); 320 _listenersAreRegistered = false; 321 } 322 } 323 324 @Override 325 public void vetoableChange(java.beans.PropertyChangeEvent evt) throws java.beans.PropertyVetoException { 326 if ("CanDelete".equals(evt.getPropertyName()) && _inUse.isInUse()) { // No I18N 327 if (evt.getOldValue() instanceof Memory) { 328 boolean doVeto = false; 329 if ((_addressing == NamedBeanAddressing.Memory) && (_memoryHandle != null) && evt.getOldValue().equals(_memoryHandle.getBean())) { 330 doVeto = true; 331 } 332 if (doVeto) { 333 PropertyChangeEvent e = new PropertyChangeEvent(this, "DoNotDelete", null, null); 334 throw new PropertyVetoException(Bundle.getMessage("MemoryInUseMemoryExpressionVeto", _base.getDisplayName()), e); // NOI18N 335 } 336 } 337 } else if ("DoDelete".equals(evt.getPropertyName())) { // No I18N 338 if (evt.getOldValue() instanceof Memory) { 339 if (evt.getOldValue().equals(_memoryHandle.getBean())) { 340 removeMemory(); 341 } 342 } 343 } 344 } 345 346 347 /** 348 * Format, parse and validate. 349 */ 350 public interface FormatterParserValidator { 351 352 /** 353 * Get the initial value 354 * @return the initial value 355 */ 356 public double getInitialValue(); 357 358 /** 359 * Format the value 360 * @param value the value 361 * @return the formatted string 362 */ 363 public String format(double value); 364 365 /** 366 * Parse the string 367 * @param str the string 368 * @return the parsed value 369 */ 370 public double parse(String str); 371 372 /** 373 * Validates the string 374 * @param str the string 375 * @return null if valid. An error message if not valid 376 */ 377 public String validate(String str); 378 } 379 380 381 public static class DefaultFormatterParserValidator 382 implements FormatterParserValidator { 383 384 @Override 385 public double getInitialValue() { 386 return 0; 387 } 388 389 @Override 390 public String format(double value) { 391 return Double.toString(value); 392 } 393 394 @Override 395 public double parse(String str) { 396 try { 397 return Double.parseDouble(str); 398 } catch (NumberFormatException e) { 399 return getInitialValue(); 400 } 401 } 402 403 @Override 404 public String validate(String str) { 405 try { 406 return null; 407 } catch (NumberFormatException e) { 408 return Bundle.getMessage("LogixNG_SelectDouble_MustBeValidInteger"); 409 } 410 } 411 412 } 413 414 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogixNG_SelectDouble.class); 415}