001package jmri.jmrit.z21server; 002 003import java.beans.PropertyChangeListener; 004import java.util.Map; 005import java.util.HashMap; 006import java.util.regex.*; 007 008import jmri.*; 009 010import org.slf4j.Logger; 011import org.slf4j.LoggerFactory; 012 013/** 014 * Handle the mapping from a Z21 turnout number to a JMRI Named Bean. This is not 015 * restricted to just JMRI turnouts, the mapping works for: 016 * - Turnouts 017 * - Lights (thrown = ON, closed = OFF 018 * - Routes (thrown sets the route, closed does nothing) 019 * - Signal Masts and Signal Heads (closed = HELD, thrown = not HELD, the aspect/appearance itself can not be modified) 020 * - Sensors (thrown = ACTIVE, closed = INACTIVE). Used to triggers all actions bound to the sensor 021 * 022 * If a component should be used by a Z21 turnout number, the number will be stored 023 * in a property of the bean. 024 * 025 * @author Eckart Meyer Copyright (C) 2025 026 */ 027 028public class TurnoutNumberMapHandler implements PropertyChangeListener { 029 030 private static TurnoutNumberMapHandler instance; 031 public final static String beanProperty = "Z21TurnoutMap"; 032 033 // NOTE: This list should match the classes used in NumberMapFrame.java 034 private final static Class<?>[] mgrList = { 035 TurnoutManager.class, 036 RouteManager.class, 037 LightManager.class, 038 SignalMastManager.class, 039 SignalHeadManager.class, 040 SensorManager.class 041 }; 042 043 private final Map<Integer, NamedBean> turnoutNumberList = new HashMap<>(); //cache for faster access 044 045 private TurnoutNumberMapHandler() { 046 } 047 048 synchronized public static TurnoutNumberMapHandler getInstance() { 049 if (instance == null) { 050 instance = new TurnoutNumberMapHandler(); 051 instance.loadNumberList(); 052 instance.addPropertyChangeListeners(); 053 } 054 return instance; 055 } 056 057 public static Class<?>[] getManagerClassList() { 058 return mgrList; 059 } 060 061/** 062 * Get the state of a component given by the Z21 Turnout Number. Get the the component from 063 * the hash map. The state is either THROWN, CLOSED or UNKNOWN. For other components than 064 * turnouts, their state will be converted into a turnout state. 065 * 066 * @param turnoutNumber - the Z21 Turnout Number 067 * @return the current state converted to a turnout state 068 */ 069 public int getStateForNumber(int turnoutNumber) { 070 int state = -1; 071 NamedBean b = turnoutNumberList.get(turnoutNumber); 072 if (b != null) { 073 log.debug("Turnout number {} is {} - {}", turnoutNumber, b.getSystemName(), b.getUserName()); 074 log.trace(" class: {}", b.getClass().getName()); 075 if (b instanceof Route) { 076 state = Turnout.CLOSED; 077 } 078 else if (b instanceof Light) { 079 Light l = (Light)b; 080 state = (l.getState() == Light.ON) ? Turnout.THROWN : Turnout.CLOSED; 081 } 082 else if (b instanceof SignalMast) { 083 SignalMast s = (SignalMast)b; 084 state = (s.getHeld()) ? Turnout.CLOSED : Turnout.THROWN; 085 } 086 else if (b instanceof SignalHead) { 087 SignalHead s = (SignalHead)b; 088 state = (s.getHeld()) ? Turnout.CLOSED : Turnout.THROWN; 089 } 090 else if (b instanceof Sensor) { 091 Sensor s = (Sensor)b; 092 state = (s.getKnownState() == Sensor.ACTIVE) ? Turnout.THROWN : Turnout.CLOSED; 093 } 094 else { 095 state = b.getState(); 096 } 097 } 098 log.debug("state for number {} is {}", turnoutNumber, state); 099 return state; 100 } 101 102/** 103 * Set the state of a component identified by the mapped number from a turnout state (THROWN or CLOSED) 104 * 105 * @param turnoutNumber - the Z21 Turnout Number 106 * @param state - a turnout state 107 */ 108 public void setStateForNumber(int turnoutNumber, int state) { 109 NamedBean b = turnoutNumberList.get(turnoutNumber); 110 if (b != null) { 111 log.debug("Turnout number {} is {} - {}", turnoutNumber, b.getSystemName(), b.getUserName()); 112 if (b instanceof Turnout) { 113 Turnout t = (Turnout)b; 114 t.setCommandedState(state); 115 } 116 else if (b instanceof Route) { 117 Route r = (Route)b; 118 if (state == Turnout.THROWN) { 119 r.setRoute(); 120 } 121 } 122 else if (b instanceof Light) { 123 Light l = (Light)b; 124 l.setState( (state == Turnout.THROWN) ? Light.ON : Light.OFF ); 125 } 126 else if (b instanceof SignalMast) { 127 SignalMast s = (SignalMast)b; 128 s.setHeld(state == Turnout.CLOSED); 129 } 130 else if (b instanceof SignalHead) { 131 SignalHead s = (SignalHead)b; 132 s.setHeld(state == Turnout.CLOSED); 133 } 134 else if (b instanceof Sensor) { 135 Sensor s = (Sensor)b; 136 try { 137 s.setKnownState((state == Turnout.THROWN) ? Sensor.ACTIVE : Sensor.INACTIVE ); 138 } 139 catch (JmriException e) { 140 log.warn("Sensor not set"); 141 } 142 } 143 } 144 } 145 146/** 147 * Load our local hash map, so we have a cache 148 */ 149 public void loadNumberList() { 150 turnoutNumberList.clear(); 151 for (Class<?> clazz : mgrList) { 152 loadNumberTable(clazz); 153 } 154 } 155 156/** 157 * Load the mapping information for all components of a given class. 158 * 159 * @param <T> 160 * @param clazz - the manager class to be used expressed as a classname, e.g. TurnoutManager.class 161 */ 162 @SuppressWarnings("unchecked") 163 private <T extends NamedBean> void loadNumberTable(Class<?> clazz) { 164 Pattern pattern = Pattern.compile("^(\\d+)$"); 165 Manager<T> mgr = (Manager<T>)InstanceManager.getNullableDefault(clazz); 166 if (mgr != null) { 167 log.trace("mgr: {} {}", mgr, mgr.getClass().getName()); 168 for (T t : mgr.getNamedBeanSet()) { 169 Object o = t.getProperty(beanProperty); 170 if (o != null) { 171 String val = o.toString(); 172 Matcher matcher = pattern.matcher(val); 173 if (matcher.matches()) { 174 if (matcher.group(0) != null) { 175 int num = Integer.parseInt(matcher.group(0)); //mapped turnout number 176 log.debug("Found number {}: {} - {}", num, t.getSystemName(), t.getUserName()); 177 turnoutNumberList.put(num, t); 178 } 179 } 180 } 181 } 182 } 183 } 184 185/** 186 * Add property change listener to all supported component managers, so we 187 * will be informed, if the tables have changed. 188 */ 189 @SuppressWarnings("unchecked") 190 private void addPropertyChangeListeners() { 191 for (Class<?> clazz : mgrList) { 192 Manager<?> mgr = (Manager<?>)InstanceManager.getNullableDefault(clazz); 193 if (mgr != null) { 194 mgr.addPropertyChangeListener(instance); 195 } 196 } 197 } 198 199/** 200 * Remove listener from all managers 201 */ 202 @SuppressWarnings("unchecked") 203 private void removePropertyChangeListeners() { 204 for (Class<?> clazz : mgrList) { 205 Manager<?> mgr = (Manager<?>)InstanceManager.getNullableDefault(clazz); 206 if (mgr != null) { 207 mgr.removePropertyChangeListener(instance); 208 } 209 } 210 } 211 212/** 213 * on destruction of the instance - would probably never occur... 214 */ 215 public void dispose() { 216 removePropertyChangeListeners(); 217 } 218 219/** 220 * Property change listener. 221 * (Re-)loads the cache from all mapped components. 222 * Also called from the UI if the mapping has been changed by the user. 223 * 224 * @param e is the propery change event 225 */ 226 @Override 227 public void propertyChange(java.beans.PropertyChangeEvent e) { 228 log.trace("property changed: {}", e.getPropertyName()); 229 loadNumberList(); //reload list 230 } 231 232 233 private final static Logger log = LoggerFactory.getLogger(TurnoutNumberMapHandler.class); 234 235}