Coverage for /home/pi/Software/model-railway-signalling/model_railway_signals/editor/objects/objects_sections.py: 100%
129 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-05 17:29 +0100
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-05 17:29 +0100
1#------------------------------------------------------------------------------------
2# This module contains all the functions for managing Track Section objects
3#------------------------------------------------------------------------------------
4#
5# External API functions intended for use by other editor modules:
6# create_section(type) - Create a default track section object on the schematic
7# delete_section(object_id) - Hard Delete an object when deleted from the schematic
8# update_section(obj_id,new_obj) - Update the configuration of an existing section object
9# paste_section(object) - Paste a copy of an object to create a new one (returns new object_id)
10# delete_section_object(object_id) - Soft delete the drawing object (prior to recreating)
11# redraw_section_object(object_id) - Redraw the object on the canvas following an update
12# default_section_object - The dictionary of default values for the object
13# mqtt_update_sections(pub_list, sub_list) - Configure MQTT publish/subscribe
14# enable_editing() - Called when 'Edit' Mode is selected (from Schematic Module)
15# disable_editing() - Called when 'Run' Mode is selected (from Schematic Module)
16#
17# Makes the following external API calls to other editor modules:
18# objects_common.set_bbox - to create/update the boundary box for the schematic object
19# objects_common.find_initial_canvas_position - to find the next 'free' canvas position
20# objects_common.new_item_id - to find the next 'free' item ID when creating objects
21# objects_common.section - To get The Object_ID for a given Item_ID
22# objects_common.section_exists - Common function to see if a given item exists
23# objects_signals.update_references_to_instrument - when the instrument ID is changed
24# objects_signals.remove_references_to_instrument - when the instrument is deleted
25# objects_sensors.update_references_to_point - called when the point ID is changed
26# objects_sensors.remove_references_to_point - called when the point is deleted
27#
28# Accesses the following external editor objects directly:
29# run_layout.schematic_callback - setting the object callbacks when created/recreated
30# objects_common.objects_common.schematic_objects - the master dictionary of Schematic Objects
31# objects_common.objects_common.section_index - The index of Section Objects (for iterating)
32# objects_common.default_object - The common dictionary element for all objects
33# objects_common.object_type - The Enumeration of supported objects
34# objects_common.canvas - Reference to the Tkinter drawing canvas
35#
36# Makes the following external API calls to library modules:
37# track_sections.delete_section(id) - delete library drawing object (part of soft delete)
38# track_sections.create_section(id) - To create the library object (create or redraw)
39# track_sections.get_boundary_box(id) - get the boundary box for the section (i.e. selection area)
40# track_sections.bind_selection_events(id) - Bind schematic events to the section "button"
41# track_sections.set_sections_to_publish_state(IDs) - configure MQTT networking
42# track_sections.subscribe_to_section_updates(node,IDs) - configure MQTT networking
43#
44#------------------------------------------------------------------------------------
46import uuid
47import copy
49from ...library import track_sections
51from . import objects_common
52from . import objects_signals
53from . import objects_sensors
54from .. import run_layout
56#------------------------------------------------------------------------------------
57# Default Track Section Objects (i.e. state at creation)
58#------------------------------------------------------------------------------------
60default_section_object = copy.deepcopy(objects_common.default_object)
61default_section_object["item"] = objects_common.object_type.section
62default_section_object["defaultlabel"] = "XXXXX"
63default_section_object["label"] = default_section_object["defaultlabel"]
64default_section_object["state"] = False
65default_section_object["editable"] = True
66default_section_object["mirror"] = ""
68#------------------------------------------------------------------------------------
69# The editing_enabled flag is used to control whether the track section object
70# is created as 'editable' or 'non-editable' (i.e. when 'running' the layout)
71#------------------------------------------------------------------------------------
73editing_enabled = True
75#------------------------------------------------------------------------------------
76# Internal function to delete/re-draw the track section objects following a mode change.
77# We delete everything first before re-drawing to keep Tkinter happy (otherwise it breaks)
78# The 'reset_state' flag is False when the objects are being re-drawn after a mode toggle
79# between edit and run mode to maintain state (improved the user experience). For all other
80# cases, the track section will be set to its default state on re-drawing (i.e. exactly
81# the same behavior as all other library objects (signals, points, instruments)
82#------------------------------------------------------------------------------------
84def redraw_all_section_objects(reset_state:bool=False):
85 for section_id in objects_common.section_index:
86 object_id = objects_common.section(section_id)
87 delete_section_object(object_id)
88 for section_id in objects_common.section_index:
89 object_id = objects_common.section(section_id)
90 redraw_section_object(object_id, reset_state=False)
91 return()
93#------------------------------------------------------------------------------------
94# Functions to set run/edit mode - We care about this for track sections as we can
95# only use library objects in run mode. In edit mode we have to use a 'fake' track
96# section object that is selectable/moveable via canvas mouse/keyboard events
97#------------------------------------------------------------------------------------
99def enable_editing():
100 global editing_enabled
101 editing_enabled = True
102 # Save the current state of the track section library objects
103 for section_id in objects_common.section_index:
104 object_id = objects_common.section(section_id)
105 current_state = track_sections.section_occupied(int(section_id))
106 current_label = track_sections.section_label(int(section_id))
107 objects_common.schematic_objects[object_id]["state"] = current_state
108 objects_common.schematic_objects[object_id]["label"] = current_label
109 # Re-draw the section objects - this will delete the library track section
110 # objects and draw dummy objects in their place
111 redraw_all_section_objects()
112 return()
114def disable_editing():
115 global editing_enabled
116 editing_enabled = False
117 # Re-draw the section objects - this will delete the dummy placeholder objects
118 # and create the 'real' library track section objects in their place
119 redraw_all_section_objects()
120 # Set the state of the track section objects to match the retained configuration
121 for section_id in objects_common.section_index:
122 object_id = objects_common.section(section_id)
123 section_label = objects_common.schematic_objects[object_id]["label"]
124 if objects_common.schematic_objects[object_id]["state"]:
125 track_sections.set_section_occupied(section_id, section_label)
126 else:
127 track_sections.clear_section_occupied(section_id, section_label)
128 return()
130#------------------------------------------------------------------------------------
131# Internal function to Update any references from other Track Sections (mirrored section)
132#------------------------------------------------------------------------------------
134def update_references_to_section(old_section_id:int, new_section_id:int):
135 # Iterate through all the sections on the schematic
136 for section_id in objects_common.section_index:
137 object_id = objects_common.section(section_id)
138 # We use strings as the IDs support local or remote sections
139 if objects_common.schematic_objects[object_id]["mirror"] == str(old_section_id):
140 objects_common.schematic_objects[object_id]["mirror"] = str(new_section_id)
141 return()
143#------------------------------------------------------------------------------------
144# Internal function to Remove any references from other Track Sections (mirrored section)
145#------------------------------------------------------------------------------------
147def remove_references_to_section(deleted_sec_id:int):
148 # Iterate through all the sections on the schematic
149 for section_id in objects_common.section_index:
150 section_object = objects_common.section(section_id)
151 # We use string comparison as the IDs support local or remote sections
152 if objects_common.schematic_objects[section_object]["mirror"] == str(deleted_sec_id):
153 objects_common.schematic_objects[section_object]["mirror"] = ""
154 return()
156#------------------------------------------------------------------------------------
157# Function to to update asection object after a configuration change
158#------------------------------------------------------------------------------------
160def update_section(object_id, new_object_configuration):
161 # We need to track whether the Item ID has changed
162 old_item_id = objects_common.schematic_objects[object_id]["itemid"]
163 new_item_id = new_object_configuration["itemid"]
164 # Delete the existing section object, copy across the new config and redraw
165 delete_section_object(object_id)
166 objects_common.schematic_objects[object_id] = copy.deepcopy(new_object_configuration)
167 redraw_section_object(object_id)
168 # Check to see if the Type-specific ID has been changed
169 if old_item_id != new_item_id:
170 # Update the type-specific index
171 del objects_common.section_index[str(old_item_id)]
172 objects_common.section_index[str(new_item_id)] = object_id
173 # Update any references to the section from the Signal / track sensor tables
174 objects_signals.update_references_to_section(old_item_id, new_item_id)
175 objects_sensors.update_references_to_section(old_item_id, new_item_id)
176 # Update any references from other Track Sections (mirrored sections)
177 update_references_to_section(old_item_id, new_item_id)
178 return()
180#------------------------------------------------------------------------------------
181# Function to redraw a Section object on the schematic. Called when the object is first
182# created or after the object configuration has been updated. The 'reset_state' flag
183# is False when the objects are being re-drawn after a mode toggle between edit and run
184# so the state is maintained to improve the user experience (when configuring/testing).
185# For all other cases, the track section will be set to its default state on re-drawing
186# (i.e. exactly the same behavior as all other library objects (signals, points etc)
187#------------------------------------------------------------------------------------
189def redraw_section_object(object_id, reset_state:bool=True):
190 global editing_enabled
191 if reset_state:
192 objects_common.schematic_objects[object_id]["state"] = default_section_object["state"]
193 objects_common.schematic_objects[object_id]["label"] = objects_common.schematic_objects[object_id]["defaultlabel"]
194 # If we are in edit mode then we draw a "dummy" Tracck Section using canvas objects
195 # so we can use the mouse events for selecting and moving them (normal Track section
196 # objects are selectable buttons which makes selection/moving overly complicated)
197 if editing_enabled:
198 # Set the tkinter 'tags' to use when creating the drawing objects
199 objects_common.schematic_objects[object_id]["tags"] = "section"+ str(objects_common.schematic_objects[object_id]["itemid"])
200 # Create the text item first using the default section label to define the width
201 text_item = objects_common.canvas.create_text(
202 objects_common.schematic_objects[object_id]["posx"],
203 objects_common.schematic_objects[object_id]["posy"],
204 text = objects_common.schematic_objects[object_id]["defaultlabel"],
205 tags=objects_common.schematic_objects[object_id]["tags"],
206 font=('Ariel',8,"normal"), fill="white")
207 # get the boundary box of the text box and use this to create the background rectangle
208 bbox = objects_common.canvas.bbox(text_item)
209 rect_item = objects_common.canvas.create_rectangle(
210 bbox[0]-4, bbox[1]-3, bbox[2]+4, bbox[3]+3,
211 tags=objects_common.schematic_objects[object_id]["tags"],
212 fill="black")
213 # raise the text item to be in front of the rectangle item
214 objects_common.canvas.tag_raise(text_item,rect_item)
215 # Now the width is set, update the section label to show the section ID
216 section_label = format(objects_common.schematic_objects[object_id]["itemid"],'02d')
217 objects_common.canvas.itemconfigure(text_item, text=section_label)
218 # Create/update the selection rectangle for the Track Section
219 objects_common.set_bbox(object_id, objects_common.canvas.bbox(objects_common.schematic_objects[object_id]["tags"]))
220 else:
221 track_sections.create_section(
222 canvas = objects_common.canvas,
223 section_id = objects_common.schematic_objects[object_id]["itemid"],
224 x = objects_common.schematic_objects[object_id]["posx"],
225 y = objects_common.schematic_objects[object_id]["posy"],
226 section_callback = run_layout.schematic_callback,
227 label = objects_common.schematic_objects[object_id]["defaultlabel"],
228 editable = objects_common.schematic_objects[object_id]["editable"])
229 # Create/update the canvas "tags" and selection rectangle for the Track Section
230 objects_common.schematic_objects[object_id]["tags"] = track_sections.get_tags(objects_common.schematic_objects[object_id]["itemid"])
231 objects_common.set_bbox(object_id, objects_common.canvas.bbox(objects_common.schematic_objects[object_id]["tags"]))
232 return()
234#------------------------------------------------------------------------------------
235# Function to Create a new default Track Section (and draw it on the canvas)
236#------------------------------------------------------------------------------------
238def create_section():
239 # Generate a new object from the default configuration with a new UUID
240 object_id = str(uuid.uuid4())
241 objects_common.schematic_objects[object_id] = copy.deepcopy(default_section_object)
242 # Find the initial canvas position for the new object and assign the item ID
243 x, y = objects_common.find_initial_canvas_position()
244 item_id = objects_common.new_item_id(exists_function=objects_common.section_exists)
245 # Add the specific elements for this particular instance of the section
246 objects_common.schematic_objects[object_id]["itemid"] = item_id
247 objects_common.schematic_objects[object_id]["posx"] = x
248 objects_common.schematic_objects[object_id]["posy"] = y
249 # Add the new object to the index of sections
250 objects_common.section_index[str(item_id)] = object_id
251 # Draw the object on the canvas
252 redraw_section_object(object_id)
253 return(object_id)
255#------------------------------------------------------------------------------------
256# Function to Paste a copy of an existing Track Section - returns the new Object ID
257# Note that only the basic section configuration is used. Underlying configuration
258# such as the current label, state and reference to any mirrored sections is set back
259# to the defaults as it will need to be configured specific to the new section
260#------------------------------------------------------------------------------------
262def paste_section(object_to_paste, deltax:int, deltay:int):
263 # Create a new UUID for the pasted object
264 new_object_id = str(uuid.uuid4())
265 objects_common.schematic_objects[new_object_id] = copy.deepcopy(object_to_paste)
266 # Assign a new type-specific ID for the object and add to the index
267 new_id = objects_common.new_item_id(exists_function=objects_common.section_exists)
268 objects_common.schematic_objects[new_object_id]["itemid"] = new_id
269 objects_common.section_index[str(new_id)] = new_object_id
270 # Set the position for the "pasted" object (offset from the original position)
271 objects_common.schematic_objects[new_object_id]["posx"] += deltax
272 objects_common.schematic_objects[new_object_id]["posy"] += deltay
273 # Now set the default values for all elements we don't want to copy:
274 objects_common.schematic_objects[new_object_id]["mirror"] = default_section_object["mirror"]
275 objects_common.schematic_objects[new_object_id]["state"] = default_section_object["state"]
276 # Copy across the default label and "reset" the actual lable to the copied default label
277 objects_common.schematic_objects[new_object_id]["label"] = objects_common.schematic_objects[new_object_id]["defaultlabel"]
278 # Set the Boundary box for the new object to None so it gets created on re-draw
279 objects_common.schematic_objects[new_object_id]["bbox"] = None
280 # Draw the new object
281 redraw_section_object(new_object_id)
282 return(new_object_id)
284#------------------------------------------------------------------------------------
285# Function to "soft delete" the section object from the canvas - Primarily used to
286# delete the track section in its current configuration prior to re-creating in its
287# new configuration - also called as part of a hard delete (below).
288#------------------------------------------------------------------------------------
290def delete_section_object(object_id):
291 track_sections.delete_section(objects_common.schematic_objects[object_id]["itemid"])
292 objects_common.canvas.delete(objects_common.schematic_objects[object_id]["tags"])
293 return()
295#------------------------------------------------------------------------------------
296# Function to 'hard delete' a track occupancy section (drawing objects and the main
297# dictionary entry). Function called when object is deleted from the schematic.
298#------------------------------------------------------------------------------------
300def delete_section(object_id):
301 # Soft delete the associated library objects from the canvas
302 delete_section_object(object_id)
303 # Remove any references to the section from the signal / track sensor tables
304 objects_signals.remove_references_to_section(objects_common.schematic_objects[object_id]["itemid"])
305 objects_sensors.remove_references_to_section(objects_common.schematic_objects[object_id]["itemid"])
306 # Remove any references from other Track Sections (mirrored sections)
307 remove_references_to_section(objects_common.schematic_objects[object_id]["itemid"])
308 # "Hard Delete" the selected object - deleting the boundary box rectangle and deleting
309 # the object from the dictionary of schematic objects (and associated dictionary keys)
310 objects_common.canvas.delete(objects_common.schematic_objects[object_id]["bbox"])
311 del objects_common.section_index[str(objects_common.schematic_objects[object_id]["itemid"])]
312 del objects_common.schematic_objects[object_id]
313 return()
315#------------------------------------------------------------------------------------
316# Function to update the MQTT networking configuration for sections, namely
317# subscribing to remote sections and setting local sections to publish state
318#------------------------------------------------------------------------------------
320def mqtt_update_sections(sections_to_publish:list, sections_to_subscribe_to:list):
321 track_sections.reset_mqtt_configuration()
322 track_sections.set_sections_to_publish_state(*sections_to_publish)
323 for section_identifier in sections_to_subscribe_to:
324 track_sections.subscribe_to_remote_section(section_identifier, run_layout.schematic_callback)
325 return()
327####################################################################################