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

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#------------------------------------------------------------------------------------ 

45 

46import uuid 

47import copy 

48 

49from ...library import track_sections 

50 

51from . import objects_common 

52from . import objects_signals 

53from . import objects_sensors 

54from .. import run_layout 

55 

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

57# Default Track Section Objects (i.e. state at creation) 

58#------------------------------------------------------------------------------------ 

59 

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"] = "" 

67 

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#------------------------------------------------------------------------------------ 

72 

73editing_enabled = True 

74 

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#------------------------------------------------------------------------------------ 

83 

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() 

92 

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#------------------------------------------------------------------------------------ 

98 

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() 

113 

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() 

129 

130#------------------------------------------------------------------------------------ 

131# Internal function to Update any references from other Track Sections (mirrored section) 

132#------------------------------------------------------------------------------------ 

133 

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() 

142 

143#------------------------------------------------------------------------------------ 

144# Internal function to Remove any references from other Track Sections (mirrored section) 

145#------------------------------------------------------------------------------------ 

146 

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() 

155 

156#------------------------------------------------------------------------------------ 

157# Function to to update asection object after a configuration change 

158#------------------------------------------------------------------------------------ 

159 

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() 

179 

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#------------------------------------------------------------------------------------ 

188 

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() 

233 

234#------------------------------------------------------------------------------------ 

235# Function to Create a new default Track Section (and draw it on the canvas) 

236#------------------------------------------------------------------------------------ 

237 

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) 

254 

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#------------------------------------------------------------------------------------ 

261 

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) 

283 

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#------------------------------------------------------------------------------------ 

289 

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() 

294 

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#------------------------------------------------------------------------------------ 

299 

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() 

314 

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#------------------------------------------------------------------------------------ 

319 

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() 

326 

327####################################################################################