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
« 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#------------------------------------------------------------------------------------
37import copy
39import tkinter as Tk
40from tkinter import ttk
42from . import common
43from . import objects
45from ..library import track_sections
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#------------------------------------------------------------------------------------
54open_windows={}
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#------------------------------------------------------------------------------------
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)
77#------------------------------------------------------------------------------------
78# Helper Function to return the list of available signal routes for the signal ahead
79#------------------------------------------------------------------------------------
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] ] )
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#------------------------------------------------------------------------------------
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] )
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)
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#------------------------------------------------------------------------------------
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)
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#------------------------------------------------------------------------------------
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)
170#####################################################################################
171# Classes for the Track Section Configuration Tab
172#####################################################################################
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#------------------------------------------------------------------------------------
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)
206 def section_exists(self,entered_value:str):
207 return (objects.section_exists(entered_value) or track_sections.section_exists(entered_value))
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#------------------------------------------------------------------------------------
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)
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)
244#------------------------------------------------------------------------------------
245# Class for the main Track Section configuration tab
246#------------------------------------------------------------------------------------
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')
275#------------------------------------------------------------------------------------
276# Top level Class for the Track Section Interlocking Tab
277#------------------------------------------------------------------------------------
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')
285#------------------------------------------------------------------------------------
286# Class for the main Track Section automation tab
287#------------------------------------------------------------------------------------
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')
307#####################################################################################
308# Top level Class for the Edit Section window
309#####################################################################################
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()
352#------------------------------------------------------------------------------------
353# Functions for Load, Save and close Window
354#------------------------------------------------------------------------------------
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()
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()
415 def close_window(self):
416 self.window.destroy()
417 del open_windows[self.object_id]
419#############################################################################################