Coverage for /home/pi/Software/model-railway-signalling/model_railway_signals/editor/configure_section.py: 94%

209 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-10 15:08 +0100

1#------------------------------------------------------------------------------------ 

2# This module contains all the ui functions for configuring Track Section objects 

3#------------------------------------------------------------------------------------ 

4# 

5# External API functions intended for use by other editor modules: 

6# edit_section - Open the edit section top level window 

7# 

8# Makes the following external API calls to other editor modules: 

9# objects.update_object(obj_id,new_obj) - Update the configuration on save 

10# objects.signal(signal_id) - To get the object_id for a given signal ID 

11# objects.track_sensor(sensor_id) - To get the object_id for a given sensor ID 

12######################################################################################################### 

13# Note that we need to use the 'objects.section_exists' function as the the library 'section_exists' 

14# function will not work in edit mode as local Track Section library objects don't exist in edit mode 

15# To be addressed in a future software update when the Track Sections functionality is re-factored 

16######################################################################################################### 

17# objects.section_exists(id) - To see if the Track Section exists ################################### 

18# 

19# Accesses the following external editor objects directly: 

20# objects.track_sensor_index - To iterate through all the track sensor objects 

21# objects.signal_index - To iterate through all the signal objects 

22# objects.schematic_objects - To load/save the object configuration 

23# 

24# Makes the following external API calls to library modules: 

25# track_sections.section_exists(id) - To see if the track section exists 

26# 

27# Inherits the following common editor base classes (from common): 

28# common.check_box 

29# common.entry_box 

30# common.str_int_item_id_entry_box 

31# common.object_id_selection 

32# common.signal_route_frame 

33# common.window_controls 

34# 

35#------------------------------------------------------------------------------------ 

36 

37import copy 

38 

39import tkinter as Tk 

40from tkinter import ttk 

41 

42from . import common 

43from . import objects 

44 

45from ..library import track_sections 

46 

47#------------------------------------------------------------------------------------ 

48# We maintain a global dictionary of open edit windows (where the key is the UUID 

49# of the object being edited) to prevent duplicate windows being opened. If the user 

50# tries to edit an object which is already being edited, then we just bring the 

51# existing edit window to the front (expanding if necessary) and set focus on it 

52#------------------------------------------------------------------------------------ 

53 

54open_windows={} 

55 

56#------------------------------------------------------------------------------------ 

57# Function to return the read-only interlocked_signals element. This is the back-reference 

58# to the signals that are configured to be interlocked with the track sections ahead 

59#------------------------------------------------------------------------------------ 

60 

61def interlocked_signals(object_id): 

62 list_of_interlocked_signals = [] 

63 for signal_id in objects.signal_index: 

64 interlocked_routes = objects.schematic_objects[objects.signal(signal_id)]["trackinterlock"] 

65 signal_routes_to_set = [False, False, False, False, False] 

66 add_signal_to_interlock_list = False 

67 for index, interlocked_route in enumerate(interlocked_routes): 

68 for interlocked_section in interlocked_route: 

69 if interlocked_section == int(objects.schematic_objects[object_id]["itemid"]): 

70 signal_routes_to_set[index] = True 

71 add_signal_to_interlock_list = True 

72 if add_signal_to_interlock_list: 

73 signal_entry = [int(signal_id), signal_routes_to_set] 

74 list_of_interlocked_signals.append(signal_entry) 

75 return(list_of_interlocked_signals) 

76 

77#------------------------------------------------------------------------------------ 

78# Helper Function to return the list of available signal routes for the signal ahead 

79#------------------------------------------------------------------------------------ 

80 

81def get_signal_routes(object_id): 

82 sig_routes = objects.schematic_objects[object_id]["sigroutes"] 

83 sub_routes = objects.schematic_objects[object_id]["subroutes"] 

84 return ( [ sig_routes[0] or sub_routes[0], 

85 sig_routes[1] or sub_routes[1], 

86 sig_routes[2] or sub_routes[2], 

87 sig_routes[3] or sub_routes[3], 

88 sig_routes[4] or sub_routes[4] ] ) 

89 

90#------------------------------------------------------------------------------------ 

91# Function to return the read-only "sensors ahead" and "sensors_behind" elements. 

92# These are the back-references to the track sensors that are configured to either 

93# set or clear the track section when the track sensor is 'passed' 

94#------------------------------------------------------------------------------------ 

95 

96def find_sensor_routes(track_section_id:int, sensor_routes:list): 

97 matched_routes = [False, False, False, False, False] 

98 one_or_more_routes_matched = False 

99 # "sensor_routes" comprises a list of routes: [main, lh1, lh2, rh1, rh2] 

100 # Each route element comprises: [[p1, p2, p3, p4, p5, p6, p7], section_id] 

101 # We need to iterate through the routes to find all matches on the section_id 

102 for index1, sensor_route in enumerate(sensor_routes): 

103 if sensor_route[1] == track_section_id: 

104 matched_routes[index1] = True 

105 one_or_more_routes_matched = True 

106 return( [one_or_more_routes_matched, matched_routes] ) 

107 

108def track_sensors_behind_and_ahead(object_id): 

109 track_section_id = int(objects.schematic_objects[object_id]["itemid"]) 

110 list_of_track_sensors_ahead = [] 

111 list_of_track_sensors_behind = [] 

112 # Iterate through all track sensor objects to see if the track section appears in the configuration 

113 for track_sensor_id in objects.track_sensor_index: 

114 routes_ahead = objects.schematic_objects[objects.track_sensor(track_sensor_id)]["routeahead"] 

115 route_matches = find_sensor_routes(track_section_id, routes_ahead) 

116 if route_matches[0]: list_of_track_sensors_ahead.append([track_sensor_id, route_matches[1]]) 

117 routes_behind = objects.schematic_objects[objects.track_sensor(track_sensor_id)]["routebehind"] 

118 route_matches = find_sensor_routes(track_section_id, routes_behind) 

119 if route_matches[0]: list_of_track_sensors_behind.append([track_sensor_id, route_matches[1]]) 

120 return(list_of_track_sensors_behind, list_of_track_sensors_ahead) 

121 

122#------------------------------------------------------------------------------------ 

123# Function to return the read-only "signals ahead" element. This is the back-reference 

124# to the signals that are configured to clear the track section when passed 

125#------------------------------------------------------------------------------------ 

126 

127def signals_ahead(object_id): 

128 list_of_signals_ahead = [] 

129 for signal_id in objects.signal_index: 

130 section_behind_signal = objects.schematic_objects[objects.signal(signal_id)]["tracksections"][0] 

131 if section_behind_signal == int(objects.schematic_objects[object_id]["itemid"]): 

132 signal_routes = get_signal_routes(objects.signal(signal_id)) 

133 list_of_signals_ahead.append([int(signal_id), signal_routes]) 

134 return(list_of_signals_ahead) 

135 

136#------------------------------------------------------------------------------------ 

137# Function to return the read-only "signals behind" and "signals_overriden" elements. 

138# These are the back-references to signals configured to set the track section to occupied 

139# when passed and any signals configured to be overridden when the section is occupied 

140#------------------------------------------------------------------------------------ 

141 

142def signals_behind_and_overridden(object_id): 

143 list_of_signals_behind = [] 

144 list_of_overridden_signals = [] 

145 for signal_id in objects.signal_index: 

146 section_id = int(objects.schematic_objects[object_id]["itemid"]) 

147 sections_ahead_of_signal = objects.schematic_objects[objects.signal(signal_id)]["tracksections"][1] 

148 override_on_occupied_flag = objects.schematic_objects[objects.signal(signal_id)]["overridesignal"] 

149 signal_routes_to_set_for_override = [False, False, False, False, False] 

150 signal_routes_to_set_for_sig_behind = [False, False, False, False, False] 

151 add_signal_to_signals_behind_list = False 

152 add_signal_to_overriden_signals_list = False 

153 for index1, signal_route in enumerate(sections_ahead_of_signal): 

154 if signal_route[0] == section_id: 

155 signal_routes_to_set_for_sig_behind[index1] = True 

156 add_signal_to_signals_behind_list = True 

157 if override_on_occupied_flag: 

158 for index2, section_ahead_of_signal in enumerate(signal_route): 

159 if section_ahead_of_signal == section_id: 

160 signal_routes_to_set_for_override[index1] = True 

161 add_signal_to_overriden_signals_list = True 

162 if add_signal_to_signals_behind_list: 

163 signal_entry = [int(signal_id), signal_routes_to_set_for_sig_behind] 

164 list_of_signals_behind.append(signal_entry) 

165 if add_signal_to_overriden_signals_list: 

166 signal_entry = [int(signal_id), signal_routes_to_set_for_override] 

167 list_of_overridden_signals.append(signal_entry) 

168 return(list_of_signals_behind, list_of_overridden_signals) 

169 

170##################################################################################### 

171# Classes for the Track Section Configuration Tab 

172##################################################################################### 

173 

174#------------------------------------------------------------------------------------ 

175# Class for the Mirror Section Entry Box - builds on the common str_int_item_id_entry_box.  

176# Class instance methods inherited/used from the parent classes are: 

177# "set_value" - set the initial value of the entry_box (str) - Also sets the 

178# current track sensor item ID (int) for validation purposes 

179# "get_value" - will return the last "valid" value of the entry box (str) 

180# "validate" - validate the section exists and not the same as the current item ID 

181#------------------------------------------------------------------------------------ 

182 

183class mirrored_section(common.str_int_item_id_entry_box): 

184 def __init__(self, parent_frame): 

185 # Create the Label Frame for the "mirrored section" entry box 

186 self.frame = Tk.LabelFrame(parent_frame, text="Link to other track section") 

187 # Create a frame for the "Section to mirror" elements 

188 self.subframe1 = Tk.Frame(self.frame) 

189 self.subframe1.pack() 

190 # Call the common base class init function to create the EB 

191 self.label1 = Tk.Label(self.subframe1,text="Section to mirror:") 

192 self.label1.pack(side=Tk.LEFT, padx=2, pady=2) 

193 ######################################################################################################### 

194 # Note that we need to use the a custom 'section_exists' function as the the library 'section_exists' 

195 # function will not work for local track sections in edit mode as the local Track Section library objects 

196 # don't exist in edit mode (although any subscribed remote track sections will exist). We therefore have 

197 # to use a combination of the 'objects.section_exists' and ' track_sections.section_exists' functions 

198 # To be addressed in a future software update when the Track Sections functionality is re-factored 

199 ######################################################################################################### 

200 super().__init__(self.subframe1, tool_tip = "Enter the ID of the track section to mirror - "+ 

201 "This can be a local section ID or a remote section ID (in the form 'Node-ID') "+ 

202 "which has been subscribed to via MQTT networking", 

203 exists_function = self.section_exists) 

204 self.pack(side=Tk.LEFT, padx=2, pady=2) 

205 

206 def section_exists(self,entered_value:str): 

207 return (objects.section_exists(entered_value) or track_sections.section_exists(entered_value)) 

208 

209#------------------------------------------------------------------------------------ 

210# Class for the Default lable entry box - builds on the common entry_box class 

211# Inherited class methods are: 

212# "set_value" - set the initial value of the entry box (string)  

213# "get_value" - get the last "validated" value of the entry box (string) 

214# Overriden class methods are 

215# "validate" - Validates the length of the entered text (between 2-10 chars) 

216#------------------------------------------------------------------------------------ 

217 

218class default_label_entry(common.entry_box): 

219 def __init__(self, parent_frame): 

220 # Create the Label Frame for the "mirrored section" entry box 

221 self.frame = Tk.LabelFrame(parent_frame, text="Default section label") 

222 self.packing1 = Tk.Label(self.frame, width=6) 

223 self.packing1.pack(side=Tk.LEFT) 

224 super().__init__(self.frame, width=16, tool_tip = "Enter the default label to "+ 

225 "display when the section is occupied (this defines the default "+ 

226 "width of the Track Section object on the schematic). The default "+ 

227 "label should be between 4 and 10 characters") 

228 self.pack(side=Tk.LEFT, padx=2, pady=2) 

229 self.packing2 = Tk.Label(self.frame, width=6) 

230 self.packing2.pack(side=Tk.LEFT) 

231 

232 def validate(self): 

233 label = self.entry.get() 

234 if len(label) >= 4 and len(label) <=10: 234 ↛ 237line 234 didn't jump to line 237, because the condition on line 234 was never false

235 valid = True 

236 else: 

237 valid = False 

238 self.TT.text = ("The default label should be between 4 and 10 characters") 

239 # If invalid and the entry is empty or spaces we need to show the error 

240 if len(label.strip())== 0: self.entry.set("#") 

241 self.set_validation_status(valid) 

242 return(valid) 

243 

244#------------------------------------------------------------------------------------ 

245# Class for the main Track Section configuration tab 

246#------------------------------------------------------------------------------------ 

247 

248class section_configuration_tab(): 

249 def __init__(self, parent_tab): 

250 # Create a Frame to hold the Section ID and General Settings 

251 self.frame1 = Tk.Frame(parent_tab) 

252 self.frame1.pack(padx=2, pady=2, fill='x') 

253 # Create the UI Element for Section ID selection 

254 ######################################################################################################### 

255 # Note that we need to use the 'objects.section_exists' function as the the library 'section_exists' 

256 # function will not work in edit mode as the Track Section library objects don't exist in edit mode 

257 # To be addressed in a future software update when the Track Sections functionality is re-factored 

258 ######################################################################################################### 

259 self.sectionid = common.object_id_selection(self.frame1, "Section ID", 

260 exists_function = objects.section_exists) 

261 self.sectionid.frame.pack(side=Tk.LEFT, padx=2, pady=2, fill='y') 

262 # Create a labelframe for the General settings 

263 self.subframe1 = Tk.LabelFrame(self.frame1, text="General Settings") 

264 self.subframe1.pack(padx=2, pady=2, fill='x') 

265 self.readonly = common.check_box(self.subframe1, label="Read only", 

266 tool_tip= "Select to make the Track Section non-editable") 

267 self.readonly.pack(padx=2, pady=2) 

268 # Create a Label Frame to hold the "Mirror" section. Note that this needs a 

269 # reference to the parent object to access the current value of Section ID 

270 self.mirror = mirrored_section(parent_tab) 

271 self.mirror.frame.pack(padx=2, pady=2, fill='x') 

272 self.label = default_label_entry(parent_tab) 

273 self.label.frame.pack(padx=2, pady=2, fill='x') 

274 

275#------------------------------------------------------------------------------------ 

276# Top level Class for the Track Section Interlocking Tab 

277#------------------------------------------------------------------------------------ 

278 

279class section_interlocking_tab(): 

280 def __init__(self, parent_tab): 

281 self.signals = common.signal_route_frame (parent_tab, label="Signals locked when section occupied", 

282 tool_tip="Edit the appropriate signals to configure interlocking") 

283 self.signals.frame.pack(padx=2, pady=2, fill='x') 

284 

285#------------------------------------------------------------------------------------ 

286# Class for the main Track Section automation tab 

287#------------------------------------------------------------------------------------ 

288 

289class section_automation_tab(): 

290 def __init__(self, parent_tab): 

291 self.behind = common.signal_route_frame (parent_tab, label="Signals controlling access into section", 

292 tool_tip="Edit the appropriate signals to configure automation") 

293 self.behind.frame.pack(padx=2, pady=2, fill='x') 

294 self.ahead = common.signal_route_frame (parent_tab, label="Signals controlling access out of section", 

295 tool_tip="Edit the appropriate signals to configure automation") 

296 self.ahead.frame.pack(padx=2, pady=2, fill='x') 

297 self.sensors1 = common.signal_route_frame (parent_tab, label="Sensors controlling access into section", 

298 tool_tip="Edit the appropriate track sensors to configure automation") 

299 self.sensors1.frame.pack(padx=2, pady=2, fill='x') 

300 self.sensors2 = common.signal_route_frame (parent_tab, label="Sensors controlling access out of section", 

301 tool_tip="Edit the appropriate track sensors to configure automation") 

302 self.sensors2.frame.pack(padx=2, pady=2, fill='x') 

303 self.override = common.signal_route_frame (parent_tab, label="Sigs overridden when section occupied", 

304 tool_tip="Edit the appropriate signals to configure automation") 

305 self.override.frame.pack(padx=2, pady=2, fill='x') 

306 

307##################################################################################### 

308# Top level Class for the Edit Section window 

309##################################################################################### 

310 

311class edit_section(): 

312 def __init__(self, root, object_id): 

313 global open_windows 

314 # If there is already a window open then we just make it jump to the top and exit 

315 if object_id in open_windows.keys(): 315 ↛ 316line 315 didn't jump to line 316, because the condition on line 315 was never true

316 open_windows[object_id].lift() 

317 open_windows[object_id].state('normal') 

318 open_windows[object_id].focus_force() 

319 else: 

320 # This is the UUID for the object being edited 

321 self.object_id = object_id 

322 # Creatre the basic Top Level window 

323 self.window = Tk.Toplevel(root) 

324 self.window.protocol("WM_DELETE_WINDOW", self.close_window) 

325 self.window.resizable(False, False) 

326 open_windows[object_id] = self.window 

327 # Create the common Apply/OK/Reset/Cancel buttons for the window (packed first to remain visible) 

328 self.controls = common.window_controls(self.window, self.load_state, self.save_state, self.close_window) 

329 self.controls.frame.pack(side=Tk.BOTTOM, padx=2, pady=2) 

330 # Create the Validation error message (this gets packed/unpacked on apply/save) 

331 self.validation_error = Tk.Label(self.window, text="Errors on Form need correcting", fg="red") 

332 # Create a frame to hold all UI elements (so they don't expand on window resize 

333 # to provide consistent behavior with the other configure object popup windows) 

334 self.main_frame = Tk.Frame(self.window) 

335 self.main_frame.pack() 

336 # Create the Notebook (for the tabs)  

337 self.tabs = ttk.Notebook(self.main_frame) 

338 # Create the Window tabs 

339 self.tab1 = Tk.Frame(self.tabs) 

340 self.tabs.add(self.tab1, text="Configuration") 

341 self.tab2 = Tk.Frame(self.tabs) 

342 self.tabs.add(self.tab2, text="Interlocking") 

343 self.tab3 = Tk.Frame(self.tabs) 

344 self.tabs.add(self.tab3, text="Automation") 

345 self.tabs.pack(fill='x') 

346 self.config = section_configuration_tab(self.tab1) 

347 self.interlocking = section_interlocking_tab(self.tab2) 

348 self.automation = section_automation_tab(self.tab3) 

349 # load the initial UI state 

350 self.load_state() 

351 

352#------------------------------------------------------------------------------------ 

353# Functions for Load, Save and close Window 

354#------------------------------------------------------------------------------------ 

355 

356 def load_state(self): 

357 # Check the section we are editing still exists (hasn't been deleted from the schematic) 

358 # If it no longer exists then we just destroy the window and exit without saving 

359 if self.object_id not in objects.schematic_objects.keys(): 359 ↛ 360line 359 didn't jump to line 360, because the condition on line 359 was never true

360 self.close_window() 

361 else: 

362 item_id = objects.schematic_objects[self.object_id]["itemid"] 

363 # Label the edit window with the Section ID 

364 self.window.title("Track Section "+str(item_id)) 

365 # Set the Initial UI state from the current object settings 

366 self.config.sectionid.set_value(item_id) 

367 self.config.readonly.set_value(not objects.schematic_objects[self.object_id]["editable"]) 

368 self.config.mirror.set_value(objects.schematic_objects[self.object_id]["mirror"], item_id) 

369 self.config.label.set_value(objects.schematic_objects[self.object_id]["defaultlabel"]) 

370 self.interlocking.signals.set_values(interlocked_signals(self.object_id)) 

371 self.automation.ahead.set_values(signals_ahead(self.object_id)) 

372 signals_behind, signals_overridden = signals_behind_and_overridden(self.object_id) 

373 self.automation.behind.set_values(signals_behind) 

374 self.automation.override.set_values(signals_overridden) 

375 sensors_behind, sensors_ahead = track_sensors_behind_and_ahead(self.object_id) 

376 self.automation.sensors1.set_values(sensors_behind) 

377 self.automation.sensors2.set_values(sensors_ahead) 

378 # Hide the validation error message 

379 self.validation_error.pack_forget() 

380 return() 

381 

382 def save_state(self, close_window:bool): 

383 # Check the section we are editing still exists (hasn't been deleted from the schematic) 

384 # If it no longer exists then we just destroy the window and exit without saving 

385 if self.object_id not in objects.schematic_objects.keys(): 385 ↛ 386line 385 didn't jump to line 386, because the condition on line 385 was never true

386 self.close_window() 

387 # Validate all user entries prior to applying the changes. Each of these would have 

388 # been validated on entry, but changes to other objects may have been made since then 

389 elif ( self.config.sectionid.validate() and self.config.mirror.validate() and 389 ↛ 412line 389 didn't jump to line 412, because the condition on line 389 was never false

390 self.config.label.validate() ): 

391 # Copy the original section Configuration (elements get overwritten as required) 

392 new_object_configuration = copy.deepcopy(objects.schematic_objects[self.object_id]) 

393 # Update the section coniguration elements from the current user selections 

394 new_object_configuration["itemid"] = self.config.sectionid.get_value() 

395 new_object_configuration["editable"] = not self.config.readonly.get_value() 

396 new_object_configuration["mirror"] = self.config.mirror.get_value() 

397 # If the default label has changed then we also need to update the actual 

398 # label if the actual label text is still set to the old default label text 

399 current_label = new_object_configuration["label"] 

400 old_default_label = new_object_configuration["defaultlabel"] 

401 new_default_label = self.config.label.get_value() 

402 new_object_configuration["defaultlabel"] = new_default_label 

403 if old_default_label != new_default_label and current_label == old_default_label: 403 ↛ 404line 403 didn't jump to line 404, because the condition on line 403 was never true

404 new_object_configuration["label"] = new_default_label 

405 # Save the updated configuration (and re-draw the object) 

406 objects.update_object(self.object_id, new_object_configuration) 

407 # Close window on "OK" or re-load UI for "apply" 

408 if close_window: self.close_window() 

409 else: self.load_state() 

410 else: 

411 # Display the validation error message 

412 self.validation_error.pack(side=Tk.BOTTOM, before=self.controls.frame) 

413 return() 

414 

415 def close_window(self): 

416 self.window.destroy() 

417 del open_windows[self.object_id] 

418 

419#############################################################################################