Coverage for /home/pi/Software/model-railway-signalling/model_railway_signals/library/points.py: 99%
240 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 is used for creating and managing point library objects on the canvas.
3#---------------------------------------------------------------------------------------------------
4#
5# External API - classes and functions (used by the Schematic Editor):
6#
7# point_type (use when creating points)
8# point_type.RH
9# point_type.LH
10#
11# point_callback_type (tells the calling program what has triggered the callback):
12# point_callback_type.point_switched (point has been switched)
13# point_callback_type.fpl_switched (facing point lock has been switched)
14#
15# create_point - Creates a point object and returns the "tag" for all tkinter canvas drawing objects
16# This allows the editor to move the point object on the schematic as required
17# Mandatory Parameters:
18# Canvas - The Tkinter Drawing canvas on which the point is to be displayed
19# point_id:int - The ID for the point - also displayed on the point button
20# pointtype:point_type - either point_type.RH or point_type.LH
21# x:int, y:int - Position of the point on the canvas (in pixels)
22# callback - the function to call on track point or FPL switched events
23# Note that the callback function returns (item_id, callback type)
24# Optional Parameters:
25# colour:str - Any tkinter colour can be specified as a string - default = "Black"
26# orientation:int- Orientation in degrees (0 or 180) - default = 0
27# reverse:bool - If the switching logic is to be reversed - Default = False
28# fpl:bool - If the point is to have a Facing point lock - Default = False (no FPL)
29# also_switch:int - the Id of another point to switch with this point - Default = None
30# auto:bool - Point is fully automatic (i.e. no point control buttons) - Default = False.
31#
32# delete_point(point_id:int) - To delete the specified point from the schematic
33#
34# update_autoswitch(point_id:int, autoswitch_id:int) - To update the 'autoswitch' reference
35#
36# lock_point(point_id:int) - use for point/signal interlocking
37#
38# unlock_point(point_id:int) - use for point/signal interlocking
39#
40# toggle_point(point_id:int) - use for route setting (use 'point_switched' to find state first)
41#
42# toggle_fpl(point_id:int) - use for route setting (use 'fpl_active' to find state first)
43#
44# point_switched(point_id:int) - returns the point state (True/False) - to support interlocking
45#
46# fpl_active(point_id:int) - returns the FPL state (True/False) - to support interlocking
47# - Will return True if the point does not have a Facing point Lock
48#
49#---------------------------------------------------------------------------------------------------
51from . import dcc_control
52from . import common
53from . import file_interface
55import tkinter as Tk
56import enum
57import logging
59# -------------------------------------------------------------------------
60# Public API classes (to be used by external functions)
61# -------------------------------------------------------------------------
63class point_type(enum.Enum):
64 RH = 1 # Right Hand point
65 LH = 2 # Left Hand point
67# Define the different callbacks types for the point
68class point_callback_type(enum.Enum):
69 point_switched = 11 # The point has been switched by the user
70 fpl_switched = 12 # The facing point lock has been switched by the user
72# -------------------------------------------------------------------------
73# Points are to be added to a global dictionary when created
74# -------------------------------------------------------------------------
76points: dict = {}
78# -------------------------------------------------------------------------
79# API Function to check if a Point exists in the dictionary of Points
80# -------------------------------------------------------------------------
82def point_exists(point_id:int):
83 if not isinstance(point_id, int):
84 logging.error("Point "+str(point_id)+": point_exists - Point ID must be an integer")
85 point_exists = False
86 else:
87 point_exists = str(point_id) in points.keys()
88 return(point_exists)
90# -------------------------------------------------------------------------
91# Callbacks for processing button pushes
92# -------------------------------------------------------------------------
94def fpl_button_event(point_id:int):
95 logging.info("Point "+str(point_id)+": FPL Button Event ************************************************************")
96 toggle_fpl(point_id)
97 points[str(point_id)]["extcallback"] (point_id,point_callback_type.fpl_switched)
98 return ()
100def change_button_event(point_id:int):
101 logging.info("Point "+str(point_id)+": Change Button Event *********************************************************")
102 toggle_point(point_id)
103 points[str(point_id)]["extcallback"] (point_id,point_callback_type.point_switched)
104 return ()
106# -------------------------------------------------------------------------
107# API Function to flip the state of the Point's Facing Point Lock (to
108# enable route setting functions. Also called when the FPL button is pressed
109# -------------------------------------------------------------------------
111def toggle_fpl(point_id:int):
112 global points
113 if not isinstance(point_id, int):
114 logging.error("Point "+str(point_id)+": toggle_fpl - Point ID must be an integer")
115 elif not point_exists(point_id):
116 logging.error("Point "+str(point_id)+": toggle_fpl - Point ID does not exist")
117 elif not points[str(point_id)]["hasfpl"]:
118 logging.error("Point "+str(point_id)+": toggle_fpl - Point does not have a Facing Point Lock")
119 else:
120 if points[str(point_id)]["locked"]:
121 logging.warning("Point "+str(point_id)+": toggle_fpl - Point is externally locked - Toggling anyway")
122 if not points[str(point_id)]["fpllock"]:
123 logging.info("Point "+str(point_id)+": Activating FPL")
124 points[str(point_id)]["changebutton"].config(state="disabled")
125 points[str(point_id)]["lockbutton"].config(relief="sunken",bg="white")
126 points[str(point_id)]["fpllock"] = True
127 else:
128 logging.info("Point "+str(point_id)+": Clearing FPL")
129 points[str(point_id)]["changebutton"].config(state="normal")
130 points[str(point_id)]["lockbutton"].config(relief="raised",bg="grey85")
131 points[str(point_id)]["fpllock"] = False
132 return()
134# -------------------------------------------------------------------------
135# Internal Function to toggle the point blade drawing objects and update
136# the internal state of the point - called by the toggle_point function
137# Can also be called on point creation to set the initial (loaded) state
138# -------------------------------------------------------------------------
140def toggle_point_state (point_id:int, switched_by_another_point:bool=False):
141 global points
142 if not points[str(point_id)]["switched"]:
143 if switched_by_another_point:
144 logging.info("Point "+str(point_id)+": Changing point to SWITCHED (switched with another point)")
145 else:
146 logging.info("Point "+str(point_id)+": Changing point to SWITCHED")
147 points[str(point_id)]["changebutton"].config(relief="sunken",bg="white")
148 points[str(point_id)]["switched"] = True
149 points[str(point_id)]["canvas"].itemconfig(points[str(point_id)]["blade2"],state="normal") #switched
150 points[str(point_id)]["canvas"].itemconfig(points[str(point_id)]["blade1"],state="hidden") #normal
151 dcc_control.update_dcc_point(point_id, True)
152 else:
153 if switched_by_another_point:
154 logging.info("Point "+str(point_id)+": Changing point to NORMAL (switched with another point)")
155 else:
156 logging.info("Point "+str(point_id)+": Changing point to NORMAL")
157 points[str(point_id)]["changebutton"].config(relief="raised",bg="grey85")
158 points[str(point_id)]["switched"] = False
159 points[str(point_id)]["canvas"].itemconfig(points[str(point_id)]["blade2"],state="hidden") #switched
160 points[str(point_id)]["canvas"].itemconfig(points[str(point_id)]["blade1"],state="normal") #normal
161 dcc_control.update_dcc_point(point_id, False)
162 return
164# -------------------------------------------------------------------------
165# Internal Function to update any downstream points (i.e. points
166# 'autoswitched' by the current point) - called on point creation
167# (if a point exists) and when a point is toggled via the API
168# -------------------------------------------------------------------------
170def update_downstream_points(point_id:int):
171 if points[str(point_id)]["alsoswitch"] != 0:
172 if not point_exists(points[str(point_id)]["alsoswitch"]):
173 logging.error("Point "+str(point_id)+": update_downstream_points - Can't 'also switch' point "
174 +str(points[str(point_id)]["alsoswitch"]) +" as that point does not exist")
175 elif not points[str(points[str(point_id)]["alsoswitch"])]["automatic"]:
176 logging.error("Point "+str(point_id)+": update_downstream_points - Can't 'also switch' point "
177 +str(points[str(point_id)]["alsoswitch"]) +" as that point is not automatic")
178 elif point_switched(point_id) != point_switched(points[str(point_id)]["alsoswitch"]):
179 logging.info("Point "+str(point_id)+": Also changing point "+str(points[str(point_id)]["alsoswitch"]))
180 # Recursively call back into the toggle_point function to change the point
181 toggle_point(points[str(point_id)]["alsoswitch"],switched_by_another_point=True)
182 return()
184# -------------------------------------------------------------------------
185# API Function to flip the route setting for the Point (to enable
186# route setting functions. Also called whenthe POINT button is pressed
187# Will also recursivelly call itself to change any "also_switch" points
188# -------------------------------------------------------------------------
190def toggle_point(point_id:int, switched_by_another_point:bool=False):
191 global points
192 if not isinstance(point_id, int):
193 logging.error("Point "+str(point_id)+": toggle_point - Point ID must be an integer")
194 elif not point_exists(point_id):
195 logging.error("Point "+str(point_id)+": toggle_point - Point ID does not exist")
196 elif points[str(point_id)]["automatic"] and not switched_by_another_point:
197 logging.error("Point "+str(point_id)+": toggle_point - Point is automatic (should be 'also switched' by another point)")
198 else:
199 if points[str(point_id)]["locked"]:
200 logging.warning("Point "+str(point_id)+": toggle_point - Point is externally locked - Toggling anyway")
201 elif points[str(point_id)]["hasfpl"] and points[str(point_id)]["fpllock"]:
202 logging.warning("Point "+str(point_id)+": toggle_point - Facing Point Lock is active - Toggling anyway")
203 # Call the internal function to toggle the point state and update the drawing objects
204 toggle_point_state(point_id,switched_by_another_point)
205 # Now change any other points we need (i.e. points switched with this one)
206 update_downstream_points(point_id)
207 return()
209# -------------------------------------------------------------------------
210# Public API function to create a Point (drawing objects + state)
211# By default the point is "NOT SWITCHED" (i.e. showing the default route)
212# If the point has a Facing Point Lock then this is set to locked
213# Function returns a list of the lines that have been drawn (so an
214# external programme can change the colours if required)
215# -------------------------------------------------------------------------
217def create_point (canvas, point_id:int, pointtype:point_type,
218 x:int, y:int, callback, colour:str="black",
219 orientation:int = 0, also_switch:int = 0,
220 reverse:bool=False, auto:bool=False, fpl:bool=False):
221 global points
222 # Set a unique 'tag' to reference the tkinter drawing objects
223 canvas_tag = "point"+str(point_id)
224 if not isinstance(point_id, int) or point_id < 1:
225 logging.error("Point "+str(point_id)+": create_point - Point ID must be a positive integer")
226 elif point_exists(point_id):
227 logging.error("Point "+str(point_id)+": create_point - Point ID already exists")
228 elif not isinstance(also_switch, int):
229 logging.error("Point "+str(point_id)+": create_point - Alsoswitch ID must be an integer")
230 elif also_switch == point_id:
231 logging.error("Point "+str(point_id)+": create_point - Alsoswitch ID is the same as the Point ID")
232 elif pointtype not in point_type:
233 logging.error("Point "+str(point_id)+": create_point - Invalid Point Type specified")
234 elif fpl and auto:
235 logging.error("Point "+str(point_id)+": create_point - Automatic point should be created without a FPL")
236 else:
237 logging.debug("Point "+str(point_id)+": Creating library object on the schematic")
238 # Create the button objects and their callbacks
239 if point_id < 10: main_button_text = "0" + str(point_id)
240 else: main_button_text = str(point_id)
241 point_button = Tk.Button (canvas, text=main_button_text, state="normal", relief="raised", 241 ↛ exitline 241 didn't jump to the function exit
242 font=('Courier',common.fontsize,"normal"),bg= "grey85",
243 padx=common.xpadding, pady=common.ypadding,
244 command = lambda:change_button_event(point_id))
245 fpl_button = Tk.Button (canvas,text="L",state="normal", relief="sunken", 245 ↛ exitline 245 didn't jump to the function exit
246 font=('Courier',common.fontsize,"normal"), bg = "white",
247 padx=common.xpadding, pady=common.ypadding,
248 command = lambda:fpl_button_event(point_id))
249 # Disable the change button if the point has FPL (default state = FPL active)
250 if fpl: point_button.config(state="disabled")
251 # Create the Tkinter drawing objects for the point
252 if pointtype==point_type.RH:
253 # Draw the lines representing a Right Hand point
254 line_coords = common.rotate_line (x,y,-25,0,-10,0,orientation)
255 blade1 = canvas.create_line (line_coords,fill=colour,width=3,tags=canvas_tag) #straignt blade
256 line_coords = common.rotate_line (x,y,-25,0,-15,+10,orientation)
257 blade2 = canvas.create_line (line_coords,fill=colour,width=3,tags=canvas_tag) #switched blade
258 line_coords = common.rotate_line (x,y,-10,0,+25,0,orientation)
259 canvas.create_line (line_coords,fill=colour,width=3,tags=canvas_tag) #straight route
260 line_coords = common.rotate_line (x,y,-15,+10,0,+25,orientation)
261 canvas.create_line(line_coords,fill=colour,width=3,tags=canvas_tag) #switched route
262 # Create the button windows in the correct relative positions for a Right Hand Point
263 point_coords = common.rotate_point (x,y,-3,-13,orientation)
264 if not auto: canvas.create_window (point_coords,anchor=Tk.W,window=point_button,tags=canvas_tag)
265 if fpl: canvas.create_window (point_coords,anchor=Tk.E,window=fpl_button,tags=canvas_tag)
266 else:
267 # Draw the lines representing a Left Hand point
268 line_coords = common.rotate_line (x,y,-25,0,-10,0,orientation)
269 blade1 = canvas.create_line (line_coords,fill=colour,width=3,tags=canvas_tag) #straignt blade
270 line_coords = common.rotate_line (x,y,-25,0,-15,-10,orientation)
271 blade2 = canvas.create_line (line_coords,fill=colour,width=3,tags=canvas_tag) #switched blade
272 line_coords = common.rotate_line (x,y,-10,0,+25,0,orientation)
273 canvas.create_line (line_coords,fill=colour,width=3,tags=canvas_tag) #straight route
274 line_coords = common.rotate_line (x,y,-15,-10,0,-25,orientation)
275 canvas.create_line(line_coords,fill=colour,width=3,tags=canvas_tag) #switched route
276 # Create the button windows in the correct relative positions for a Left Hand Point
277 point_coords = common.rotate_point (x,y,-3,+13,orientation)
278 if not auto: canvas.create_window (point_coords,anchor=Tk.W,window=point_button,tags=canvas_tag)
279 if fpl: canvas.create_window (point_coords,anchor=Tk.E,window=fpl_button,tags=canvas_tag)
280 # The "normal" state of the point is the straight through route by default
281 # With reverse set to True, the divergent route becomes the "normal" state
282 if reverse is True: blade1, blade2 = blade2, blade1
283 # Hide the line for the switched route (display it later when we need it)
284 canvas.itemconfig(blade2,state="hidden")
285 # Compile a dictionary of everything we need to track
286 points[str(point_id)] = {}
287 points[str(point_id)]["canvas"] = canvas # Tkinter canvas object
288 points[str(point_id)]["blade1"] = blade1 # Tkinter drawing object
289 points[str(point_id)]["blade2"] = blade2 # Tkinter drawing object
290 points[str(point_id)]["changebutton"] = point_button # Tkinter drawing object
291 points[str(point_id)]["lockbutton"] = fpl_button # Tkinter drawing object
292 points[str(point_id)]["extcallback"] = callback # The callback to make on an event
293 points[str(point_id)]["alsoswitch"] = also_switch # Point to automatically switch (0=none)
294 points[str(point_id)]["automatic"] = auto # Whether the point is automatic or not
295 points[str(point_id)]["hasfpl"] = fpl # Whether the point has a FPL or not
296 points[str(point_id)]["fpllock"] = fpl # Initial state of the FPL (locked if it has FPL)
297 points[str(point_id)]["locked"] = False # Initial "interlocking" state of the point
298 points[str(point_id)]["switched"] = False # Initial "switched" state of the point
299 points[str(point_id)]["tags"] = canvas_tag # Canvas Tags for all drawing objects
300 # Get the initial state for the point (if layout state has been successfully loaded)
301 # if nothing has been loaded then the default state (as created) will be applied
302 loaded_state = file_interface.get_initial_item_state("points",point_id)
303 # Toggle the FPL if FPL is ACTIVE ("switched" will be 'None' if no data was loaded)
304 # We toggle on False as points with FPLs are created with the FPL active by default
305 if fpl and loaded_state["fpllock"] == False: toggle_fpl(point_id)
306 # Toggle the point state if SWITCHED ("switched" will be 'None' if no data was loaded)
307 # Note that Toggling the point will also send the DCC commands to set the initial state
308 # If we don't toggle the point we need to send out the DCC commands for the default state
309 if loaded_state["switched"]: toggle_point_state(point_id)
310 else: dcc_control.update_dcc_point(point_id,False)
311 # Externally lock the point if required
312 if loaded_state["locked"]: lock_point(point_id)
313 # We need to ensure that all points in an 'auto switch' chain are set to the same
314 # switched/not-switched state so they switch together correctly. First, we test to
315 # see if any existing points have already been configured to "autoswitch' the newly
316 # created point and, if so, toggle the newly created point to the same state
317 for other_point_id in points:
318 if points[other_point_id]["alsoswitch"] == point_id:
319 update_downstream_points(int(other_point_id))
320 # Update any downstream points (configured to be 'autoswitched' by this point
321 # but only if they have been created (allows them to be created after this point)
322 if point_exists(points[str(point_id)]["alsoswitch"]):
323 validate_alsoswitch_point(point_id, also_switch)
324 update_downstream_points(point_id)
325 # Return the canvas_tag for the tkinter drawing objects
326 return(canvas_tag)
328# -------------------------------------------------------------------------
329# Public API function to Lock a points (Warning generated if APL and not FPL active)
330# -------------------------------------------------------------------------
332def lock_point(point_id:int):
333 global points
334 if not isinstance(point_id, int):
335 logging.error("Point "+str(point_id)+": lock_point - Point ID must be an integer")
336 elif not point_exists(point_id):
337 logging.error("Point "+str(point_id)+": lock_point - Point ID does not exist")
338 elif not points[str(point_id)]["locked"]:
339 logging.info ("Point "+str(point_id)+": Locking point")
340 if not points[str(point_id)]["hasfpl"]:
341 # If the point doesn't have a FPL we just inhibit the change button
342 points[str(point_id)]["changebutton"].config(state="disabled")
343 elif not points[str(point_id)]["fpllock"]:
344 # If the FPL is not already active then we need to activate it (with a warning)
345 logging.warning ("Point "+str(point_id)+": lock_point - Activating FPL before locking")
346 toggle_fpl (point_id)
347 # Now inhibit the FPL button to stop it being manually unlocked
348 points[str(point_id)]["lockbutton"].config(state="disabled")
349 points[str(point_id)]["locked"] = True
350 return()
352# -------------------------------------------------------------------------
353# API function to Unlock a point
354# -------------------------------------------------------------------------
356def unlock_point(point_id:int):
357 global points
358 if not isinstance(point_id, int):
359 logging.error("Point "+str(point_id)+": unlock_point - Point ID must be an integer")
360 elif not point_exists(point_id):
361 logging.error("Point "+str(point_id)+": unlock_point - Point ID does not exist")
362 elif points[str(point_id)]["locked"]:
363 logging.info("Point "+str(point_id)+": Unlocking point")
364 if not points[str(point_id)]["hasfpl"]:
365 # If the point doesn't have FPL we need to re-enable the change button
366 points[str(point_id)]["changebutton"].config(state="normal")
367 else:
368 # If the point has FPL we just need to re-enable the FPL button
369 points[str(point_id)]["lockbutton"].config(state="normal")
370 points[str(point_id)]["locked"] = False
371 return()
373# -------------------------------------------------------------------------
374# API function to Return the current state of the point
375# -------------------------------------------------------------------------
377def point_switched(point_id:int):
378 if not isinstance(point_id, int):
379 logging.error("Point "+str(point_id)+": point_switched - Point ID must be an integer")
380 switched = False
381 elif not point_exists(point_id):
382 logging.error("Point "+str(point_id)+": point_switched - Point ID does not exist")
383 switched = False
384 else:
385 switched = points[str(point_id)]["switched"]
386 return(switched)
388# -------------------------------------------------------------------------
389# API function to query the current state of the FPL (no FPL will return True)
390# -------------------------------------------------------------------------
392def fpl_active(point_id:int):
393 if not isinstance(point_id, int):
394 logging.error("Point "+str(point_id)+": fpl_active - Point ID must be an integer")
395 locked = False
396 elif not point_exists(point_id):
397 logging.error("Point "+str(point_id)+": fpl_active - Point ID does not exist")
398 locked = False
399 elif not points[str(point_id)]["hasfpl"]:
400 # Point does not have a FPL - always return True in this case
401 locked = True
402 else:
403 locked = points[str(point_id)]["fpllock"]
404 return(locked)
406# ------------------------------------------------------------------------------------------
407# API function for deleting a point library object (including all the drawing objects).
408# This is used by the schematic editor for changing point types where we delete the existing
409# point with all its data and then recreate it (with the same ID) in its new configuration.
410# ------------------------------------------------------------------------------------------
412def delete_point(point_id:int):
413 global points
414 if not isinstance(point_id, int):
415 logging.error("Point "+str(point_id)+": delete_point - Point ID must be an integer")
416 elif not point_exists(point_id):
417 logging.error("Point "+str(point_id)+": delete_point - Point ID does not exist")
418 else:
419 logging.debug("Point "+str(point_id)+": Deleting library object from the schematic")
420 # Delete all the tkinter drawing objects associated with the point
421 points[str(point_id)]["canvas"].delete(points[str(point_id)]["tags"])
422 points[str(point_id)]["changebutton"].destroy()
423 points[str(point_id)]["lockbutton"].destroy()
424 # Delete the point entry from the dictionary of points
425 del points[str(point_id)]
426 return()
428# ------------------------------------------------------------------------------------------
429# API function for updating the ID of the point to be 'autoswitched' by a point without
430# needing to delete the point and then create it in its new state. The main use case is
431# when bulk deleting objects via the schematic editor, where we want to avoid interleaving
432# tkinter 'create' commands in amongst the 'delete' commands outside of the main tkinter
433# loop as this can lead to problems with artefacts persisting on the canvas.
434# ------------------------------------------------------------------------------------------
436def update_autoswitch(point_id:int, autoswitch_id:int):
437 if not isinstance(point_id, int):
438 logging.error("Point "+str(point_id)+": update_autoswitch - Point ID must be an integer")
439 elif not point_exists(point_id):
440 logging.error("Point "+str(point_id)+": update_autoswitch - Point ID does not exist")
441 elif not isinstance(autoswitch_id, int):
442 logging.error("Point "+str(point_id)+": update_autoswitch - Autoswitch ID must be an integer")
443 elif autoswitch_id > 0 and not point_exists(autoswitch_id):
444 logging.error("Point "+str(point_id)+": update_autoswitch - Autoswitch ID does not exist")
445 else:
446 logging.debug("Point "+str(point_id)+": Updating Autoswitch point to "+str(autoswitch_id))
447 points[str(point_id)]["alsoswitch"] = autoswitch_id
448 if point_exists(points[str(point_id)]["alsoswitch"]):
449 validate_alsoswitch_point(point_id, autoswitch_id)
450 update_downstream_points(point_id)
451 return()
453# ------------------------------------------------------------------------------------------
454# Internal common function to validate point linking (raising a warning as required)
455# ------------------------------------------------------------------------------------------
457def validate_alsoswitch_point(point_id:int, autoswitch_id:int):
458 for other_point in points:
459 if points[other_point]['alsoswitch'] == autoswitch_id and other_point != str(point_id):
460 # We've found another point 'also switching' the point we are trying to link to
461 logging.warning("Point "+str(point_id)+": configuring to 'autoswitch' "+str(autoswitch_id)+
462 " - but point "+ other_point+" is also configured to 'autoswitch' "+str(autoswitch_id))
463 return()
465###############################################################################