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

300 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-03-22 13:55 +0000

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

2# This module contains all the functions for the menubar utilities 

3# 

4# Classes (pop up windows) called from the main editor module menubar selections 

5# dcc_programming(root, dcc_programming_enabled_function, dcc_power_on_function, dcc_power_off_function) 

6# dcc_mappings(root) 

7# 

8# Uses the following library functions: 

9# pi_sprog_interface.service_mode_read_cv(cv_to_read) 

10# pi_sprog_interface.service_mode_write_cv(cv_to_write,value_to_write) 

11# pi_sprog_interface.send_accessory_short_event(address, state) 

12# pi_sprog_interface.request_dcc_power_off() 

13# pi_sprog_interface.request_dcc_power_on() 

14# dcc_control.get_dcc_address_mappings() 

15# 

16# Uses the following common editor UI elements: 

17# common.entry_box 

18# common.integer_entry_box 

19# common.scrollable_text_frame 

20# common.CreateToolTip 

21# common.dcc_entry_box 

22#------------------------------------------------------------------------------------ 

23 

24import tkinter as Tk 

25import json 

26import os 

27 

28from ..library import pi_sprog_interface 

29from ..library import dcc_control 

30from . import common 

31 

32#------------------------------------------------------------------------------------ 

33# Class for a CV Programming entry element 

34#------------------------------------------------------------------------------------ 

35 

36class cv_programming_entry(): 

37 def __init__(self, parent_frame, row): 

38 self.configuration_variable = common.integer_entry_box(parent_frame, width=5, min_value=1, 

39 max_value=1023, callback=self.cv_updated, allow_empty=True, empty_equals_zero=False, 

40 tool_tip="Enter the number of the Configuration Variable (CV) to read or program") 

41 self.configuration_variable.grid(column=0, row=row) 

42 self.current_value = common.entry_box(parent_frame, width=5, 

43 tool_tip="Last Read value of CV (select 'Read' to populate or refresh)") 

44 self.current_value.configure(state="disabled", disabledforeground="black") 

45 self.current_value.grid(column=1, row=row) 

46 self.value_to_set = common.integer_entry_box(parent_frame, width=5, min_value=0, max_value=255, 

47 allow_empty=True, empty_equals_zero=False, callback=self.value_updated, 

48 tool_tip="Enter the new value to set (select 'write' to program)") 

49 self.value_to_set.grid(column=2, row=row) 

50 self.notes = common.entry_box(parent_frame, width=30, 

51 tool_tip="Add notes for this CV / value") 

52 self.notes.grid(column=3, row=row, sticky="ew") 

53 

54 def validate(self): 

55 # No need to validate the current value as this is read only 

56 return(self.configuration_variable.validate() and self.value_to_set.validate()) 

57 

58 def cv_updated(self): 

59 # If the CV entry has been updated set the current value back to unknown 

60 # and set the colour of the value_to_set back to black (to be programmed) 

61 self.current_value.set_value("") 

62 self.value_to_set.config(fg="black") 

63 

64 def value_updated(self): 

65 # If the CV value_to_set has been updated set the colour back to black 

66 self.value_to_set.config(fg="black") 

67 

68#------------------------------------------------------------------------------------ 

69# Class for a grid of CV Programming entry elements (uxses class above) 

70#------------------------------------------------------------------------------------ 

71 

72class cv_programming_grid(): 

73 def __init__(self, parent_frame): 

74 self.grid_frame = Tk.Frame(parent_frame) 

75 self.grid_frame.pack(fill='x') 

76 self.list_of_entries = [] 

77 number_of_columns = 2 

78 number_of_rows = 15 

79 # Create the columns of CV programming values 

80 for column_index in range(number_of_columns): 

81 # Enable the column to expand to fill the available space 

82 self.grid_frame.columnconfigure(column_index, weight=1) 

83 # Create a frame to hold the columns of values (allow it to expand) 

84 # Also allow the 3rd column (holding the notes) to expand within it 

85 self.frame = Tk.Frame(self.grid_frame) 

86 self.frame.grid(row=0, column=column_index, padx=10, sticky="ew") 

87 self.frame.columnconfigure(3, weight=1) 

88 # Create the heading labels for the cv_programming_entry elements 

89 # Pack the "Notes" heading so it can expand to fill the available space 

90 self.label1 = Tk.Label(self.frame,text="CV") 

91 self.label1.grid(row=0, column=0) 

92 self.label2 = Tk.Label(self.frame,text="Value") 

93 self.label2.grid(row=0, column=1) 

94 self.label3 = Tk.Label(self.frame,text="New") 

95 self.label3.grid(row=0, column=2) 

96 self.label4 = Tk.Label(self.frame,text="Notes") 

97 self.label4.grid(row=0, column=3, sticky="ew") 

98 # Create the cv_programming_entry element 

99 for row_index in range(number_of_rows): 

100 self.list_of_entries.append(cv_programming_entry(self.frame,row=row_index+1)) 

101 

102 def validate(self): 

103 valid=True 

104 for cv_entry_element in self.list_of_entries: 

105 if not cv_entry_element.validate(): valid=False 

106 return(valid) 

107 

108#------------------------------------------------------------------------------------ 

109# Class for the CV Programming UI Element (uses class above) 

110#------------------------------------------------------------------------------------ 

111 

112class cv_programming_element(): 

113 def __init__(self, root_window, parent_window, parent_frame, dcc_programming_enabled_function, 

114 dcc_power_off_function, dcc_power_on_function): 

115 self.dcc_programming_enabled_function = dcc_programming_enabled_function 

116 self.dcc_power_off_function = dcc_power_off_function 

117 self.dcc_power_on_function = dcc_power_on_function 

118 self.root_window = root_window 

119 self.parent_window = parent_window 

120 # Default CV configuration filename 

121 self.loaded_file = "" 

122 # Create the warning text 

123 self.label=Tk.Label(parent_frame,text="WARNING - Before programming CVs, ensure only the device to be "+ 

124 "programmed\nis connected to the DCC bus - all other devices should be disconnected", fg="red") 

125 self.label.pack(padx=2, pady=2) 

126 # Create the grid of CVs to programe 

127 self.cv_grid = cv_programming_grid (parent_frame) 

128 # Create the Read/Write Buttons and the status label in a subframe to center them 

129 self.subframe1 = Tk.Frame(parent_frame) 

130 self.subframe1.pack() 

131 self.B1 = Tk.Button (self.subframe1, text = "Read CVs",command=self.read_all_cvs) 

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

133 self.TT1 = common.CreateToolTip(self.B1, "Read all CVs to retrieve / refresh the current values") 

134 self.B2 = Tk.Button (self.subframe1, text = "Write CVs",command=self.write_all_cvs) 

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

136 self.TT2 = common.CreateToolTip(self.B2, "Write all CVs to set the new values") 

137 self.status = Tk.Label(self.subframe1, width=45, borderwidth=1, relief="solid") 

138 self.status.pack(side=Tk.LEFT, padx=2, pady=2, expand='y') 

139 self.statusTT = common.CreateToolTip(self.status, "Displays the CV Read / Write progress and status") 

140 # Create the notes/documentation text entry 

141 self.notes = common.scrollable_text_frame(parent_frame, max_height=10, max_width=38, 

142 min_height=5, min_width=38, editable=True, auto_resize=True) 

143 self.notes.pack(padx=2, pady=2, fill='both', expand=True) 

144 self.notes.set_value("Document your CV configuration here") 

145 # Create the Save/load Buttons and the filename label in a subframe to center them 

146 self.subframe2 = Tk.Frame(parent_frame) 

147 self.subframe2.pack(fill='y') 

148 self.B3 = Tk.Button (self.subframe2, text = "Open",command=self.load_config) 

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

150 self.TT3 = common.CreateToolTip(self.B3, "Load a CV configuration from file") 

151 self.B4 = Tk.Button (self.subframe2, text = "Save",command=lambda:self.save_config(save_as=False)) 

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

153 self.TT4 = common.CreateToolTip(self.B4, "Save the current CV configuration to file") 

154 self.B5 = Tk.Button (self.subframe2, text = "Save as",command=lambda:self.save_config(save_as=True)) 

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

156 self.TT5 = common.CreateToolTip(self.B5, "Save the current CV configuration as a new file") 

157 self.name=Tk.Label(self.subframe2, width=45, borderwidth=1, relief="solid") 

158 self.name.pack(side=Tk.LEFT, padx=2, pady=2, expand='y') 

159 self.nameTT = common.CreateToolTip(self.name, "Displays the name of the CV config file after save or load") 

160 

161 def read_all_cvs(self): 

162 # Force a focus out event to "accept" all values before programming (if the focus out 

163 # event is processed after programming it will be interpreted as the CV being updated 

164 # which will then set the read value back to blank as the CV value may have been changed 

165 self.B1.focus_set() 

166 self.root_window.update() 

167 # Check programmng is enabled (DCC power on - which implies SPROG is connected 

168 if not self.dcc_programming_enabled_function(): 

169 self.status.config(text="Connect to SPROG and enable DCC power to read CVs", fg="red") 

170 elif not self.cv_grid.validate(): 

171 self.status.config(text="Entries on form need correcting", fg="red") 

172 else: 

173 self.status.config(text="Reading CVs", fg="black") 

174 for cv_entry_element in self.cv_grid.list_of_entries: 

175 cv_entry_element.current_value.set_value("") 

176 self.root_window.update_idletasks() 

177 read_errors = False 

178 for cv_entry_element in self.cv_grid.list_of_entries: 

179 cv_to_read = cv_entry_element.configuration_variable.get_value() 

180 if cv_to_read is not None: 

181 cv_value = pi_sprog_interface.service_mode_read_cv(cv_to_read) 

182 if cv_value is not None: 

183 cv_entry_element.current_value.set_value(str(cv_value)) 

184 else: 

185 cv_entry_element.current_value.set_value("---") 

186 read_errors = True 

187 self.root_window.update_idletasks() 

188 if read_errors: 

189 self.status.config(text="One or more CVs could not be read", fg="red") 

190 else: 

191 self.status.config(text="") 

192 # Cycle the power to enable the changes (revert back to normal operation) 

193 pi_sprog_interface.request_dcc_power_off() 

194 pi_sprog_interface.request_dcc_power_on() 

195 

196 def write_all_cvs(self): 

197 # Force a focus out event to "accept" all values before programming (if the focus out 

198 # event is processed after programming it will be interpreted as the value being updated 

199 # which will then set the colour of the value back to black as it may have been changed 

200 self.B1.focus_set() 

201 self.root_window.update() 

202 # Check programmng is enabled (DCC power on - which implies SPROG is connected 

203 if not self.dcc_programming_enabled_function(): 

204 self.status.config(text="Connect to SPROG and enable DCC power to write CVs", fg="red") 

205 elif not self.cv_grid.validate(): 

206 self.status.config(text="Entries on form need correcting", fg="red") 

207 else: 

208 self.status.config(text="Writing CVs", fg="black") 

209 for cv_entry_element in self.cv_grid.list_of_entries: 

210 cv_entry_element.value_to_set.config(fg="black") 

211 self.root_window.update_idletasks() 

212 write_errors = False 

213 for cv_entry_element in self.cv_grid.list_of_entries: 

214 cv_to_write = cv_entry_element.configuration_variable.get_value() 

215 value_to_write = cv_entry_element.value_to_set.get_value() 

216 if cv_to_write is not None and value_to_write is not None: 

217 write_success = pi_sprog_interface.service_mode_write_cv(cv_to_write,value_to_write) 

218 if write_success: 

219 cv_entry_element.value_to_set.config(fg="green") 

220 else: 

221 cv_entry_element.value_to_set.config(fg="red") 

222 write_errors = True 

223 self.root_window.update_idletasks() 

224 if write_errors: 

225 self.status.config(text="One or more CVs could not be written", fg="red") 

226 else: 

227 self.status.config(text="") 

228 # Cycle the power to enable the changes (revert back to normal operation) 

229 pi_sprog_interface.request_dcc_power_off() 

230 pi_sprog_interface.request_dcc_power_on() 

231 

232 def save_config(self, save_as:bool): 

233 self.B4.focus_set() 

234 self.root_window.update() 

235 if not self.cv_grid.validate(): 

236 self.status.config(text="Entries on form need correcting", fg="red") 

237 else: 

238 self.status.config(text="") 

239 # Filename to save is the filename loaded - or ask the user 

240 if self.loaded_file == "" or save_as: 

241 initial_filename = os.path.split(self.loaded_file)[1] 

242 filename_to_save=Tk.filedialog.asksaveasfilename(title='Save CV Configuration', parent=self.parent_window, 

243 filetypes=(('CV configuration files','*.cvc'),('all files','*.*')),initialfile=initial_filename) 

244 # Set the filename to blank if the user has cancelled out of (or closed) the dialogue 

245 if filename_to_save == (): filename_to_save = "" 

246 # If the filename is not blank enforce the '.cvc' extention 

247 if filename_to_save != "" and not filename_to_save.endswith(".cvc"): filename_to_save.append(".cvc") 

248 else: 

249 filename_to_save = self.loaded_file 

250 # Only continue (to save the file) if the filename is not blank 

251 if filename_to_save != "": 

252 # Create a json structure to save the data  

253 data_to_save = {} 

254 data_to_save["filename"] = filename_to_save 

255 data_to_save["documentation"] = self.notes.get_value() 

256 data_to_save["configuration"] = [] 

257 for cv_entry_element in self.cv_grid.list_of_entries: 

258 cv_number = cv_entry_element.configuration_variable.get_value() 

259 cv_value = cv_entry_element.value_to_set.get_value() 

260 cv_notes = cv_entry_element.notes.get_value() 

261 data_to_save["configuration"].append([cv_number,cv_value,cv_notes]) 

262 try: 

263 file_contents = json.dumps(data_to_save,indent=3,sort_keys=False) 

264 except Exception as exception: 

265 Tk.messagebox.showerror(parent=self.parent_window,title="Data Error",message=str(exception)) 

266 else: 

267 # write the json structure to file 

268 try: 

269 with open (filename_to_save,'w') as file: file.write(file_contents) 

270 file.close 

271 except Exception as exception: 

272 Tk.messagebox.showerror(parent=self.parent_window,title="File Save Error",message=str(exception)) 

273 else: 

274 self.loaded_file = filename_to_save 

275 self.name.config(text="Configuration file: "+os.path.split(self.loaded_file)[1]) 

276 

277 

278 def load_config(self): 

279 self.B4.focus_set() 

280 self.root_window.update() 

281 filename_to_load = Tk.filedialog.askopenfilename(parent=self.parent_window,title='Load CV configuration', 

282 filetypes=(('cvc files','*.cvc'),('all files','*.*')),initialdir = '.') 

283 # Set the filename to blank if the user has cancelled out of (or closed) the dialogue 

284 if filename_to_load == (): filename_to_load = "" 

285 # Only continue (to load the file) if the filename is not blank 

286 if filename_to_load != "": 

287 try: 

288 with open (filename_to_load,'r') as file: loaded_data=file.read() 

289 file.close 

290 except Exception as exception: 

291 Tk.messagebox.showerror(parent=self.parent_window,title="File Load Error", message=str(exception)) 

292 else: 

293 try: 

294 loaded_data = json.loads(loaded_data) 

295 except Exception as exception: 

296 Tk.messagebox.showerror(parent=self.parent_window,title="File Parse Error", message=str(exception)) 

297 else: 

298 self.loaded_file = filename_to_load 

299 self.name.config(text="Configuration file: "+os.path.split(self.loaded_file)[1]) 

300 self.notes.set_value(loaded_data["documentation"]) 

301 for index, cv_entry_element in enumerate(loaded_data["configuration"]): 

302 self.cv_grid.list_of_entries[index].configuration_variable.set_value(cv_entry_element[0]) 

303 self.cv_grid.list_of_entries[index].value_to_set.set_value(cv_entry_element[1]) 

304 self.cv_grid.list_of_entries[index].notes.set_value(cv_entry_element[2]) 

305 self.cv_grid.list_of_entries[index].current_value.set_value("") 

306 

307#------------------------------------------------------------------------------------ 

308# Class for the "one touch" Programming UI Element (uses class above) 

309#------------------------------------------------------------------------------------ 

310 

311class one_touch_programming_element(): 

312 def __init__(self, parent_frame, dcc_programming_enabled_function): 

313 self.dcc_programming_enabled_function = dcc_programming_enabled_function 

314 # Create the Address and entry Buttons (in a subframe to center them) 

315 self.subframe = Tk.Frame(parent_frame) 

316 self.subframe.pack() 

317 self.label = Tk.Label(self.subframe, text="Address to program") 

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

319 self.entry = common.dcc_entry_box(self.subframe) 

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

321 self.B1 = Tk.Button (self.subframe, text = "On (fwd)",command=lambda:self.send_command(True)) 

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

323 self.TT1 = common.CreateToolTip(self.B1, "Send an ON command to the selected DCC address") 

324 self.B2 = Tk.Button (self.subframe, text = "Off (rev)",command=lambda:self.send_command(False)) 

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

326 self.TT2 = common.CreateToolTip(self.B2, "Send an OFF command to the selected DCC address") 

327 # Create the Status Label 

328 self.status = Tk.Label(parent_frame, text="") 

329 self.status.pack(padx=2, pady=2) 

330 

331 def send_command(self, command): 

332 self.subframe.focus_set() 

333 # Check programmng is enabled (DCC power on - which implies SPROG is connected 

334 if not self.dcc_programming_enabled_function(): 

335 self.status.config(text="Connect to SPROG and enable DCC power to programme", fg="red") 

336 elif not self.entry.validate() or self.entry.get_value() < 1: 

337 self.status.config(text="Entered DCC address is invalid", fg="red") 

338 else: 

339 self.status.config(text="") 

340 pi_sprog_interface.send_accessory_short_event(self.entry.get_value(), command) 

341 

342#------------------------------------------------------------------------------------ 

343# Class for the "DCC Programming" window - Uses the classes above 

344# Note that if a window is already open then we just raise it and exit 

345#------------------------------------------------------------------------------------ 

346 

347dcc_programming_window = None 

348 

349class dcc_programming(): 

350 def __init__(self, root_window, dcc_programming_enabled_function, dcc_power_off_function, dcc_power_on_function): 

351 global dcc_programming_window 

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

353 if dcc_programming_window is not None: 

354 dcc_programming_window.lift() 

355 dcc_programming_window.state('normal') 

356 dcc_programming_window.focus_force() 

357 else: 

358 # Create the top level window for DCC Programming  

359 self.window = Tk.Toplevel(root_window) 

360 self.window.title("DCC Programming") 

361 self.window.protocol("WM_DELETE_WINDOW", self.ok) 

362 self.window.resizable(False, False) 

363 dcc_programming_window = self.window 

364 # Create the ok/close button and tooltip - pack first so it remains visible on re-sizing 

365 self.B1 = Tk.Button (self.window, text = "Ok / Close", command=self.ok) 

366 self.TT1 = common.CreateToolTip(self.B1, "Close window") 

367 self.B1.pack(padx=5, pady=5, side=Tk.BOTTOM) 

368 # Create an overall frame to pack everything else in 

369 self.frame = Tk.Frame(self.window) 

370 self.frame.pack(fill='both', expand=True) 

371 # Create the labelframe for "one Touch" DCC Programming (this gets packed later) 

372 self.labelframe1 = Tk.LabelFrame(self.frame, text="DCC One Touch Programming") 

373 self.labelframe1.pack(padx=2, pady=2, fill='x') 

374 self.one_touch_programming = one_touch_programming_element(self.labelframe1, dcc_programming_enabled_function) 

375 # Create the labelframe for CV Programming (this gets packed later) 

376 self.labelframe2 = Tk.LabelFrame(self.frame, text="DCC Configuration Variable (CV) Programming") 

377 self.labelframe2.pack(padx=2, pady=2, fill='both', expand=True) 

378 self.cv_programming = cv_programming_element(root_window, self.window, self.labelframe2, 

379 dcc_programming_enabled_function, dcc_power_off_function, dcc_power_on_function) 

380 

381 def ok(self): 

382 global dcc_programming_window 

383 dcc_programming_window = None 

384 self.window.destroy() 

385 

386#------------------------------------------------------------------------------------ 

387# Class for the "DCC Mappings" window  

388# Note that if a window is already open then we just raise it and exit 

389#------------------------------------------------------------------------------------ 

390 

391dcc_mappings_window = None 

392 

393class dcc_mappings(): 

394 def __init__(self, root_window): 

395 global dcc_mappings_window 

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

397 if dcc_mappings_window is not None: 

398 dcc_mappings_window.lift() 

399 dcc_mappings_window.state('normal') 

400 dcc_mappings_window.focus_force() 

401 else: 

402 # Create the top level window  

403 self.window = Tk.Toplevel(root_window) 

404 self.window.title("DCC Mappings") 

405 self.window.protocol("WM_DELETE_WINDOW", self.ok) 

406 self.window.resizable(False, False) 

407 dcc_mappings_window = self.window 

408 # Create the ok/close and refresh buttons - pack first so they remain visible on re-sizing 

409 self.frame1 = Tk.Frame(self.window) 

410 self.frame1.pack(fill='x', expand=True, side=Tk.BOTTOM) 

411 # Create a subframe to center the buttons in 

412 self.subframe = Tk.Frame(self.frame1) 

413 self.subframe.pack() 

414 self.B1 = Tk.Button (self.subframe, text = "Ok / Close",command=self.ok) 

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

416 self.TT1 = common.CreateToolTip(self.B1, "Close window") 

417 self.B2 = Tk.Button (self.subframe, text = "Refresh",command=self.refresh_display) 

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

419 self.TT1 = common.CreateToolTip(self.B2, "Reload the current DCC address mappings") 

420 # Create an overall frame to pack everything else in 

421 self.mappings_frame = None 

422 self.refresh_display() 

423 

424 def ok(self): 

425 global dcc_mappings_window 

426 dcc_mappings_window = None 

427 self.window.destroy() 

428 

429 def refresh_display(self): 

430 # Create a frame to hold the mappings (destroy the old one first if needed) 

431 if self.mappings_frame is not None: 

432 self.mappings_frame.destroy() 

433 self.mappings_frame = Tk.Frame(self.window) 

434 self.mappings_frame.pack(fill='both', expand=True) 

435 #create a subframe  

436 # Retrieve the sorted dictionary of DCC address mappings 

437 dcc_address_mappings = dcc_control.get_dcc_address_mappings() 

438 # If there are no mappings then just display a warning 

439 if len(dcc_address_mappings) == 0: 

440 label = Tk.Label(self.mappings_frame, text="No DCC address mappings defined") 

441 label.pack(padx=20, pady=20) 

442 else: 

443 # Build the table of DCC mappings 

444 row_index = 0 

445 for dcc_address in dict(sorted(dcc_address_mappings.items())): 

446 if row_index == 0: 

447 column_frame = Tk.LabelFrame(self.mappings_frame, text="DCC addresses") 

448 column_frame.pack(side=Tk.LEFT, pady=2, padx=2, fill='both', expand=True, anchor='n') 

449 # Create a subframe for the row (pack in the column frame) 

450 row_frame = Tk.Frame(column_frame) 

451 row_frame.pack(fill='x') 

452 # Create the labels with the DCC mapping text (pack in the row subframe) 

453 mapping_text = u"\u2192"+" "+dcc_address_mappings[dcc_address][0]+" "+str(dcc_address_mappings[dcc_address][1]) 

454 label1 = Tk.Label(row_frame, width=5, text=dcc_address, anchor="e") 

455 label1.pack(side=Tk.LEFT) 

456 label2 = Tk.Label(row_frame, width=10, text=mapping_text, anchor="w") 

457 label2.pack(side=Tk.LEFT) 

458 row_index = row_index + 1 

459 if row_index >= 20: row_index = 0 

460 

461#############################################################################################