/ lib / python / gladevcp / offsetpage_widget.py
offsetpage_widget.py
  1  #!/usr/bin/env python
  2  # GladeVcp Widget - offsetpage
  3  #
  4  # Copyright (c) 2013 Chris Morley
  5  #
  6  # This program is free software: you can redistribute it and/or modify
  7  # it under the terms of the GNU General Public License as published by
  8  # the Free Software Foundation, either version 2 of the License, or
  9  # (at your option) any later version.
 10  #
 11  # This program is distributed in the hope that it will be useful,
 12  # but WITHOUT ANY WARRANTY; without even the implied warranty of
 13  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 14  # GNU General Public License for more details.
 15  
 16  # This widget reads the offsets for current tool, current G5x, G92 using python library linuxcnc.
 17  # all other offsets are read directly from the Var file, if available, as linuxcnc does not give
 18  # access to all offsets thru NML, only current ones.
 19  # you can hide any axes or any columns
 20  # set metric or imperial
 21  # set the var file to search
 22  # set the text formatting for metric/imperial separately
 23  
 24  import sys, os, pango, linuxcnc
 25  from hal_glib import GStat
 26  datadir = os.path.abspath(os.path.dirname(__file__))
 27  AXISLIST = ['offset', 'X', 'Y', 'Z', 'A', 'B', 'C', 'U', 'V', 'W', 'name']
 28  # we need to know if linuxcnc isn't running when using the GLADE editor
 29  # as it causes big delays in response
 30  lncnc_running = False
 31  try:
 32      import gobject, gtk
 33  except:
 34      print('GTK not available')
 35      sys.exit(1)
 36  
 37  # localization
 38  import locale
 39  BASE = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), ".."))
 40  LOCALEDIR = os.path.join(BASE, "share", "locale")
 41  locale.setlocale(locale.LC_ALL, '')
 42  
 43  # we put this in a try so there is no error in the glade editor
 44  # linuxcnc is probably not running then
 45  try:
 46      INIPATH = os.environ['INI_FILE_NAME']
 47  except:
 48      pass
 49  
 50  class OffsetPage(gtk.VBox):
 51      __gtype_name__ = 'OffsetPage'
 52      __gproperties__ = {
 53          'display_units_mm' : (gobject.TYPE_BOOLEAN, 'Display Units', 'Display in metric or not',
 54                      False, gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
 55          'mm_text_template' : (gobject.TYPE_STRING, 'Text template for Metric Units',
 56                  'Text template to display. Python formatting may be used for one variable',
 57                  "%10.3f", gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
 58          'imperial_text_template' : (gobject.TYPE_STRING, 'Text template for Imperial Units',
 59                  'Text template to display. Python formatting may be used for one variable',
 60                  "%9.4f", gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
 61          'font' : (gobject.TYPE_STRING, 'Pango Font', 'Display font to use',
 62                  "sans 12", gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
 63          'highlight_color'  : (gtk.gdk.Color.__gtype__, 'Highlight color', "",
 64                      gobject.PARAM_READWRITE),
 65          'foreground_color'  : (gtk.gdk.Color.__gtype__, 'Active text color', "",
 66                      gobject.PARAM_READWRITE),
 67          'hide_columns' : (gobject.TYPE_STRING, 'Hidden Columns', 'A no-spaces list of axes to hide: xyzabcuvw and t are the options',
 68                      "", gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
 69          'hide_rows' : (gobject.TYPE_STRING, 'Hidden Rows', 'A no-spaces list of rows to hide: 0123456789abc are the options' ,
 70                      "", gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
 71      }
 72      __gproperties = __gproperties__
 73  
 74      __gsignals__ = {
 75                      'selection_changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (gobject.TYPE_STRING, gobject.TYPE_STRING,)),
 76                     }
 77  
 78  
 79      def __init__(self, filename = None, *a, **kw):
 80          super(OffsetPage, self).__init__()
 81          self.gstat = GStat()
 82          self.filename = filename
 83          self.linuxcnc = linuxcnc
 84          self.status = linuxcnc.stat()
 85          self.cmd = linuxcnc.command()
 86          self.hash_check = None
 87          self.display_units_mm = 0 # imperial
 88          self.machine_units_mm = 0 # imperial
 89          self.program_units = 0 # imperial
 90          self.display_follows_program = False # display units are chosen indepenadently of G20/G21
 91          self.font = "sans 12"
 92          self.editing_mode = False
 93          self.highlight_color = gtk.gdk.Color("lightblue")
 94          self.foreground_color = gtk.gdk.Color("red")
 95          self.unselectable_color = gtk.gdk.Color("lightgray")
 96          self.hidejointslist = []
 97          self.hidecollist = []
 98          self.wTree = gtk.Builder()
 99          self.wTree.set_translation_domain("linuxcnc") # for locale translations
100          self.wTree.add_from_file(os.path.join(datadir, "offsetpage.glade"))
101          self.current_system = None
102          self.selection_mask = ()
103          self.axisletters = ["x", "y", "z", "a", "b", "c", "u", "v", "w"]
104  
105          # global references
106          self.store = self.wTree.get_object("liststore2")
107          self.all_window = self.wTree.get_object("all_window")
108          self.view2 = self.wTree.get_object("treeview2")
109          self.view2.connect('button_press_event', self.on_treeview2_button_press_event)
110          self.selection = self.view2.get_selection()
111          self.selection.set_mode(gtk.SELECTION_SINGLE)
112          self.selection.connect("changed", self.on_selection_changed)
113          self.modelfilter = self.wTree.get_object("modelfilter")
114          self.edit_button = self.wTree.get_object("edit_button")
115          self.edit_button.connect('toggled', self.set_editing)
116          zero_g92_button = self.wTree.get_object("zero_g92_button")
117          zero_g92_button.connect('clicked', self.zero_g92)
118          zero_rot_button = self.wTree.get_object("zero_rot_button")
119          zero_rot_button.connect('clicked', self.zero_rot)
120          self.set_font(self.font)
121          self.modelfilter.set_visible_column(10)
122          self.buttonbox = self.wTree.get_object("buttonbox")
123          for col, name in enumerate(AXISLIST):
124              if col > 9:break
125              temp = self.wTree.get_object("cell_%s" % name)
126              temp.connect('edited', self.col_editted, col)
127          temp = self.wTree.get_object("cell_name")
128          temp.connect('edited', self.col_editted, 10)
129          # reparent offsetpage box from Glades top level window to widgets VBox
130          window = self.wTree.get_object("offsetpage_box")
131          window.reparent(self)
132  
133          # check the ini file if UNITS are set to mm
134          # first check the global settings
135          # if not available then the X axis units
136          try:
137              self.inifile = self.linuxcnc.ini(INIPATH)
138              units = self.inifile.find("TRAJ", "LINEAR_UNITS")
139              if units == None:
140                  units = self.inifile.find("AXIS_X", "UNITS")
141          except:
142              print _("**** Offsetpage widget ERROR: LINEAR_UNITS not found in INI's TRAJ section")
143              units = "inch"
144  
145          # now setup the conversion array depending on the machine native units
146          if units == "mm" or units == "metric" or units == "1.0":
147              self.machine_units_mm = 1
148              self.conversion = [1.0 / 25.4] * 3 + [1] * 3 + [1.0 / 25.4] * 3
149          else:
150              self.machine_units_mm = 0
151              self.conversion = [25.4] * 3 + [1] * 3 + [25.4] * 3
152  
153          # check linuxcnc status every half second
154          gobject.timeout_add(500, self.periodic_check)
155  
156      # Reload the offsets into display
157      def reload_offsets(self):
158          g54, g55, g56, g57, g58, g59, g59_1, g59_2, g59_3 = self.read_file()
159          if g54 == None: return
160          # Get the offsets arrays and convert the units if the display
161          # is not in machine native units
162          g5x = self.status.g5x_offset
163          tool = self.status.tool_offset
164          g92 = self.status.g92_offset
165          rot = self.status.rotation_xy
166  
167          if self.display_units_mm != self.machine_units_mm:
168              g5x = self.convert_units(g5x)
169              tool = self.convert_units(tool)
170              g92 = self.convert_units(g92)
171              g54 = self.convert_units(g54)
172              g55 = self.convert_units(g55)
173              g56 = self.convert_units(g56)
174              g57 = self.convert_units(g57)
175              g58 = self.convert_units(g58)
176              g59 = self.convert_units(g59)
177              g59_1 = self.convert_units(g59_1)
178              g59_2 = self.convert_units(g59_2)
179              g59_3 = self.convert_units(g59_3)
180  
181          # set the text style based on unit type
182          if self.display_units_mm:
183              tmpl = self.mm_text_template
184          else:
185              tmpl = self.imperial_text_template
186  
187          degree_tmpl = "%11.2f"
188  
189          # fill each row of the liststore fron the offsets arrays
190          for row, i in enumerate([tool, g5x, rot, g92, g54, g55, g56, g57, g58, g59, g59_1, g59_2, g59_3]):
191              for column in range(0, 9):
192                  if row == 2:
193                      if column == 2:
194                          self.store[row][column + 1] = locale.format(degree_tmpl, rot)
195                      else:
196                          self.store[row][column + 1] = " "
197                  else:
198                      self.store[row][column + 1] = locale.format(tmpl, i[column])
199              # set the current system's label's color - to make it stand out a bit
200              if self.store[row][0] == self.current_system:
201                  self.store[row][13] = self.foreground_color
202              else:
203                  self.store[row][13] = None
204              # mark unselectable rows a dirrerent color
205              if self.store[row][0] in self.selection_mask:
206                  self.store[row][12] = self.unselectable_color
207  
208      # This is for adding a filename path after the offsetpage is already loaded.
209      def set_filename(self, filename):
210          self.filename = filename
211          self.reload_offsets()
212  
213      # We read the var file directly
214      # and pull out the info we need
215      # if anything goes wrong we set all the info to 0
216      def read_file(self):
217          try:
218              g54 = [0, 0, 0, 0, 0, 0, 0, 0, 0]
219              g55 = [0, 0, 0, 0, 0, 0, 0, 0, 0]
220              g56 = [0, 0, 0, 0, 0, 0, 0, 0, 0]
221              g57 = [0, 0, 0, 0, 0, 0, 0, 0, 0]
222              g58 = [0, 0, 0, 0, 0, 0, 0, 0, 0]
223              g59 = [0, 0, 0, 0, 0, 0, 0, 0, 0]
224              g59_1 = [0, 0, 0, 0, 0, 0, 0, 0, 0]
225              g59_2 = [0, 0, 0, 0, 0, 0, 0, 0, 0]
226              g59_3 = [0, 0, 0, 0, 0, 0, 0, 0, 0]
227              if self.filename == None:
228                  return g54, g55, g56, g57, g58, g59, g59_1, g59_2, g59_3
229              if not os.path.exists(self.filename):
230                  return g54, g55, g56, g57, g58, g59, g59_1, g59_2, g59_3
231              logfile = open(self.filename, "r").readlines()
232              for line in logfile:
233                  temp = line.split()
234                  param = int(temp[0])
235                  data = float(temp[1])
236  
237                  if 5229 >= param >= 5221:
238                      g54[param - 5221] = data
239                  elif 5249 >= param >= 5241:
240                      g55[param - 5241] = data
241                  elif 5269 >= param >= 5261:
242                      g56[param - 5261] = data
243                  elif 5289 >= param >= 5281:
244                      g57[param - 5281] = data
245                  elif 5309 >= param >= 5301:
246                      g58[param - 5301] = data
247                  elif 5329 >= param >= 5321:
248                      g59[param - 5321] = data
249                  elif 5349 >= param >= 5341:
250                      g59_1[param - 5341] = data
251                  elif 5369 >= param >= 5361:
252                      g59_2[param - 5361] = data
253                  elif 5389 >= param >= 5381:
254                      g59_3[param - 5381] = data
255              return g54, g55, g56, g57, g58, g59, g59_1, g59_2, g59_3
256          except:
257              return None, None, None, None, None, None, None, None, None
258  
259      # This allows hiding or showing columns from a text string of columnns
260      # eg list ='ab'
261      # default, all the columns are shown
262      def set_col_visible(self, list, bool):
263          try:
264              for index in range(0, len(list)):
265                  colstr = str(list[index])
266                  colnum = "xyzabcuvwt".index(colstr.lower())
267                  name = AXISLIST[colnum + 1]
268                  renderer = self.wTree.get_object(name)
269                  renderer.set_property('visible', bool)
270          except:
271              pass
272  
273      # hide/show the offset rows from a text string of row ids
274      # eg list ='123'
275      def set_row_visible(self, list, bool):
276          try:
277              for index in range(0, len(list)):
278                  rowstr = str(list[index])
279                  rownum = "0123456789abcd".index(rowstr.lower())
280                  self.store[rownum][10] = bool
281          except:
282              pass
283  
284      # This does the units conversion
285      # it just multiplies the two arrays
286      def convert_units(self, v):
287          c = self.conversion
288          return map(lambda x, y: x * y, v, c)
289  
290      # make the cells editable and highlight them
291      def set_editing(self, widget):
292          state = widget.get_active()
293          # stop updates from linuxcnc
294          self.editing_mode = state
295          # highlight editable rows
296          if state:
297              color = self.highlight_color
298          else:
299              color = None
300          # Set rows editable
301          for i in range(1, 13):
302              if not self.store[i][0] in('G5x', 'Rot', 'G92', 'G54', 'G55', 'G56', 'G57', 'G58', 'G59', 'G59.1', 'G59.2', 'G59.3'): continue
303              if self.store[i][0] in self.selection_mask: continue
304              self.store[i][11] = state
305              self.store[i][12] = color
306          self.queue_draw()
307  
308      # When the column is edited this does the work
309      # TODO the edited column does not end up showing the editted number even though linuxcnc
310      # registered the change
311      def col_editted(self, widget, filtered_path, new_text, col):
312          (store_path,) = self.modelfilter.convert_path_to_child_path(filtered_path)
313          row = store_path
314          axisnum = col - 1
315          # print "EDITED:", new_text, col, int(filtered_path), row, "axis num:", axisnum
316  
317          def system_to_p(system):
318              convert = { "G54":1, "G55":2, "G56":3, "G57":4, "G58":5, "G59":6, "G59.1":7, "G59.2":8, "G59.3":9}
319              try:
320                  pnum = convert[system]
321              except:
322                  pnum = None
323              return pnum
324  
325          # Hack to not edit any rotational offset but Z axis
326          if row == 2 and not col == 3: return
327  
328          # set the text style based on unit type
329          if self.display_units_mm:
330              tmpl = lambda s: self.mm_text_template % s
331          else:
332              tmpl = lambda s: self.imperial_text_template % s
333  
334          # allow 'name' columnn text to be arbitrarily changed
335          if col == 10:
336              self.store[row][14] = new_text
337              return
338          # set the text in the table
339          try:
340              self.store[row][col] = locale.format("%10.4f", locale.atof(new_text))
341          except:
342              print _("offsetpage widget error: unrecognized float input")
343          # make sure we switch to correct units for machine and rotational, row 2, does not get converted
344          try:
345              if not self.display_units_mm == self.program_units and not row == 2:
346                  if self.program_units == 1:
347                      convert = 25.4
348                  else:
349                      convert = 1.0 / 25.4
350                  qualified = float(locale.atof(new_text)) * convert
351              else:
352                  qualified = float(locale.atof(new_text))
353          except:
354              print 'error'
355          # now update linuxcnc to the change
356          try:
357              global lncnc_runnning
358              if lncnc_running:
359                  if self.status.task_mode != self.linuxcnc.MODE_MDI:
360                      self.cmd.mode(self.linuxcnc.MODE_MDI)
361                      self.cmd.wait_complete()
362                  if row == 1:
363                      self.cmd.mdi("G10 L2 P0 %s %10.4f" % (self.axisletters[axisnum], qualified))
364                  elif row == 2:
365                      if col == 3:
366                          self.cmd.mdi("G10 L2 P0 R %10.4f" % (qualified))
367                  elif row == 3:
368                      self.cmd.mdi("G92 %s %10.4f" % (self.axisletters[axisnum], qualified))
369                  else:
370                      pnum = system_to_p(self.store[row][0])
371                      if not pnum == None:
372                          self.cmd.mdi("G10 L2 P%d %s %10.4f" % (pnum, self.axisletters[axisnum], qualified))
373                  self.cmd.mode(self.linuxcnc.MODE_MANUAL)
374                  self.cmd.wait_complete()
375                  self.cmd.mode(self.linuxcnc.MODE_MDI)
376                  self.cmd.wait_complete()
377                  self.gstat.emit('reload-display')
378          except:
379              print _("offsetpage widget error: MDI call error")
380              self.reload_offsets()
381  
382  
383      # callback to cancel G92 when button pressed
384      def zero_g92(self, widget):
385          # print "zero g92"
386          if lncnc_running:
387              try:
388                  if self.status.task_mode != self.linuxcnc.MODE_MDI:
389                      self.cmd.mode(self.linuxcnc.MODE_MDI)
390                      self.cmd.wait_complete()
391                  self.cmd.mdi("G92.1")
392                  self.cmd.mode(self.linuxcnc.MODE_MANUAL)
393                  self.cmd.wait_complete()
394                  self.cmd.mode(self.linuxcnc.MODE_MDI)
395                  self.cmd.wait_complete()
396                  self.gstat.emit('reload-display')
397              except:
398                  print _("MDI error in offsetpage widget -zero G92")
399  
400      # callback to zero rotational offset when button pressed
401      def zero_rot(self, widget):
402          # print "zero rotation offset"
403          if lncnc_running:
404              try:
405                  if self.status.task_mode != self.linuxcnc.MODE_MDI:
406                      self.cmd.mode(self.linuxcnc.MODE_MDI)
407                      self.cmd.wait_complete()
408                  self.cmd.mdi("G10 L2 P0 R 0")
409                  self.cmd.mode(self.linuxcnc.MODE_MANUAL)
410                  self.cmd.wait_complete()
411                  self.cmd.mode(self.linuxcnc.MODE_MDI)
412                  self.cmd.wait_complete()
413                  self.gstat.emit('reload-display')
414              except:
415                  print _("MDI error in offsetpage widget-zero rotational offset")
416  
417      # check for linnuxcnc ON and IDLE which is the only safe time to edit the tool file.
418      # if in editing mode don't update else you can't actually edit
419      def periodic_check(self):
420          convert = ("None", "G54", "G55", "G56", "G57", "G58", "G59", "G59.1", "G59.2", "G59.3")
421          try:
422              self.status.poll()
423              on = self.status.task_state > linuxcnc.STATE_OFF
424              idle = self.status.interp_state == linuxcnc.INTERP_IDLE
425              self.edit_button.set_sensitive(bool(on and idle))
426              self.current_system = convert[self.status.g5x_index]
427              self.program_units = int(self.status.program_units == 2)
428              if self.display_follows_program:
429                  self.display_units_mm = self.program_units
430              global lncnc_running
431              lncnc_running = True
432          except:
433              self.current_system = "G54"
434              lncnc_running = False
435  
436          if self.filename and not self.editing_mode:
437              self.reload_offsets()
438          return True
439  
440      # sets the color when editing is active
441      def set_highlight_color(self, value):
442          self.highlight_color = gtk.gdk.Color(value)
443  
444      # sets the text color of the current system description name
445      def set_foreground_color(self, value):
446          self.foreground_color = gtk.gdk.Color(value)
447  
448      # Allows you to set the text font of all the rows and columns
449      def set_font(self, value):
450          for col, name in enumerate(AXISLIST):
451              if col > 10:break
452              temp = self.wTree.get_object("cell_" + name)
453              temp.set_property('font', value)
454  
455      # helper function to set the units to inch
456      def set_to_inch(self):
457          self.display_units_mm = 0
458  
459      # helper function to set the units to mm
460      def set_to_mm(self):
461          self.display_units_mm = 1
462  
463      def set_display_follows_program_units(self):
464          self.display_follows_program = True
465  
466      def set_display_independent_units(self):
467          self.display_follows_program = False
468  
469      # helper function to hide control buttons
470      def hide_buttonbox(self, state):
471          if state:
472              self.buttonbox.hide()
473          else:
474              self.buttonbox.show()
475  
476      # Mark the active system with cursor highlight
477      def mark_active(self, system):
478          try:
479              pathlist = []
480              for row in self.modelfilter:
481                  if row[0] == system:
482                      pathlist.append(row.path)
483              if len(pathlist) == 1:
484                  self.selection.select_path(pathlist[0])
485          except:
486              print _("offsetpage_widget error: cannot select coordinate system"), system
487  
488      # Get the selected row the user clicked
489      def get_selected(self):
490          model, iter = self.selection.get_selected()
491          if iter:
492              system = model.get_value(iter, 0)
493              name = model.get_value(iter, 14)
494              # print "System:%s Name:%s"% (system,name)
495              return system, name
496          else:
497              return None, None
498  
499      def on_selection_changed(self, treeselection):
500          system, name = self.get_selected()
501          # print self.status.g5x_index
502          if system in self.selection_mask:
503              self.mark_active(self.current_system)
504          self.emit("selection_changed", system, name)
505  
506      def set_names(self, names):
507          for offset, name in names:
508              for row in range(0, 13):
509                  if offset == self.store[row][0]:
510                      self.store[row][14] = name
511  
512      def get_names(self):
513          temp = []
514          for row in range(0, 13):
515              temp.append([self.store[row][0], self.store[row][14]])
516          return temp
517  
518      # For single click selection when in edit mode
519      def on_treeview2_button_press_event(self, widget, event):
520          if event.button == 1 : # left click
521              try:
522                  path, model, x, y = widget.get_path_at_pos(int(event.x), int(event.y))
523                  self.view2.set_cursor(path, None, True)
524              except:
525                  pass
526  
527      # standard Gobject method
528      def do_get_property(self, property):
529          name = property.name.replace('-', '_')
530          if name in self.__gproperties.keys():
531              return getattr(self, name)
532          else:
533              raise AttributeError('unknown property %s' % property.name)
534  
535      # standard Gobject method
536      # This is so that in the Glade editor, you can change the display
537      def do_set_property(self, property, value):
538          name = property.name.replace('-', '_')
539          if name == 'font':
540              try:
541                  self.set_font(value)
542              except:
543                  pass
544          if name == 'hide_columns':
545              self.set_col_visible("xyzabcuvwt", True)
546              self.set_col_visible("%s" % value, False)
547          if name == 'hide_rows':
548              self.set_row_visible("0123456789abc", True)
549              self.set_row_visible("%s" % value, False)
550          if name in self.__gproperties.keys():
551              setattr(self, name, value)
552  
553      # boiler code for variable access
554      def __getitem__(self, item):
555          return getattr(self, item)
556      def __setitem__(self, item, value):
557          return setattr(self, item, value)
558  
559  
560  # for testing without glade editor:
561  # Must linuxcnc running to see anything
562  def main(filename = None):
563      window = gtk.Dialog("My dialog",
564                     None,
565                     gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
566                     (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
567                      gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
568      offsetpage = OffsetPage()
569  
570      window.vbox.add(offsetpage)
571      # offsetpage.set_filename("../../../configs/sim/gscreen_custom/sim.var")
572      # offsetpage.set_col_visible("Yabuvw", False)
573      # offsetpage.set_row_visible("456789abc", False)
574      # offsetpage.set_row_visible("89abc", True)
575      # offsetpage.set_to_mm()
576      # offsetpage.set_font("sans 20")
577      # offsetpage.set_property("highlight_color", gtk.gdk.Color('blue'))
578      # offsetpage.set_highlight_color("violet")
579      # offsetpage.set_foreground_color("yellow")
580      # offsetpage.mark_active("G55")
581      # offsetpage.selection_mask = ("Tool", "Rot", "G5x")
582      # offsetpage.set_names([['G54', 'Default'], ["G55", "Vice1"], ['Rot', 'Rotational']])
583      # print offsetpage.get_names()
584  
585      window.connect("destroy", gtk.main_quit)
586      window.show_all()
587      response = window.run()
588      if response == gtk.RESPONSE_ACCEPT:
589         print "True"
590      else:
591         print "False"
592  
593  if __name__ == "__main__":
594      main()
595  
596