model.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732
  1. """!
  2. @package gmodeler.model
  3. @brief wxGUI Graphical Modeler (base classes)
  4. Classes:
  5. - Model
  6. (C) 2010-2011 by the GRASS Development Team
  7. This program is free software under the GNU General Public License
  8. (>=v2). Read the file COPYING that comes with GRASS for details.
  9. @author Martin Landa <landa.martin gmail.com>
  10. """
  11. import os
  12. import getpass
  13. import copy
  14. import re
  15. try:
  16. import xml.etree.ElementTree as etree
  17. except ImportError:
  18. import elementtree.ElementTree as etree # Python <= 2.4
  19. import wx
  20. from core.globalvar import ETCWXDIR
  21. from core.gcmd import GMessage, GException, GError, RunCommand
  22. from grass.script import core as grass
  23. from grass.script import task as gtask
  24. class Model(object):
  25. """!Class representing the model"""
  26. def __init__(self, canvas = None):
  27. self.items = list() # list of actions/loops/...
  28. # model properties
  29. self.properties = { 'name' : _("model"),
  30. 'description' : _("Script generated by wxGUI Graphical Modeler."),
  31. 'author' : getpass.getuser() }
  32. # model variables
  33. self.variables = dict()
  34. self.variablesParams = dict()
  35. self.canvas = canvas
  36. def GetCanvas(self):
  37. """!Get canvas or None"""
  38. return self.canvas
  39. def GetItems(self, objType = None):
  40. """!Get list of model items
  41. @param objType Object type to filter model objects
  42. """
  43. if not objType:
  44. return self.items
  45. result = list()
  46. for item in self.items:
  47. if isinstance(item, objType):
  48. result.append(item)
  49. return result
  50. def GetItem(self, aId):
  51. """!Get item of given id
  52. @param aId item id
  53. @return Model* instance
  54. @return None if no item found
  55. """
  56. ilist = self.GetItems()
  57. for item in ilist:
  58. if item.GetId() == aId:
  59. return item
  60. return None
  61. def GetNumItems(self, actionOnly = False):
  62. """!Get number of items"""
  63. if actionOnly:
  64. return len(self.GetItems(objType = ModelAction))
  65. return len(self.GetItems())
  66. def GetNextId(self):
  67. """!Get next id (data ignored)
  68. @return next id to be used (default: 1)
  69. """
  70. if len(self.items) < 1:
  71. return 1
  72. currId = self.items[-1].GetId()
  73. if currId > 0:
  74. return currId + 1
  75. return 1
  76. def GetProperties(self):
  77. """!Get model properties"""
  78. return self.properties
  79. def GetVariables(self, params = False):
  80. """!Get model variables"""
  81. if params:
  82. return self.variablesParams
  83. return self.variables
  84. def SetVariables(self, data):
  85. """!Set model variables"""
  86. self.variables = data
  87. def Reset(self):
  88. """!Reset model"""
  89. self.items = list()
  90. def RemoveItem(self, item):
  91. """!Remove item from model
  92. @return list of related items to remove/update
  93. """
  94. relList = list()
  95. upList = list()
  96. if not isinstance(item, ModelData):
  97. self.items.remove(item)
  98. if isinstance(item, ModelAction):
  99. for rel in item.GetRelations():
  100. relList.append(rel)
  101. data = rel.GetData()
  102. if len(data.GetRelations()) < 2:
  103. relList.append(data)
  104. else:
  105. upList.append(data)
  106. elif isinstance(item, ModelData):
  107. for rel in item.GetRelations():
  108. relList.append(rel)
  109. if rel.GetFrom() == self:
  110. relList.append(rel.GetTo())
  111. else:
  112. relList.append(rel.GetFrom())
  113. elif isinstance(item, ModelLoop):
  114. for rel in item.GetRelations():
  115. relList.append(rel)
  116. for action in self.GetItems():
  117. action.UnSetBlock(item)
  118. return relList, upList
  119. def FindAction(self, aId):
  120. """!Find action by id"""
  121. alist = self.GetItems(objType = ModelAction)
  122. for action in alist:
  123. if action.GetId() == aId:
  124. return action
  125. return None
  126. def GetData(self):
  127. """!Get list of data items"""
  128. result = list()
  129. dataItems = self.GetItems(objType = ModelData)
  130. for action in self.GetItems(objType = ModelAction):
  131. for rel in action.GetRelations():
  132. dataItem = rel.GetData()
  133. if dataItem not in result:
  134. result.append(dataItem)
  135. if dataItem in dataItems:
  136. dataItems.remove(dataItem)
  137. # standalone data
  138. if dataItems:
  139. result += dataItems
  140. return result
  141. def FindData(self, value, prompt):
  142. """!Find data item in the model
  143. @param value value
  144. @param prompt prompt
  145. @return ModelData instance
  146. @return None if not found
  147. """
  148. for data in self.GetData():
  149. if data.GetValue() == value and \
  150. data.GetPrompt() == prompt:
  151. return data
  152. return None
  153. def LoadModel(self, filename):
  154. """!Load model definition stored in GRASS Model XML file (gxm)
  155. @todo Validate against DTD
  156. Raise exception on error.
  157. """
  158. dtdFilename = os.path.join(ETCWXDIR, "xml", "grass-gxm.dtd")
  159. # parse workspace file
  160. try:
  161. gxmXml = ProcessModelFile(etree.parse(filename))
  162. except StandardError, e:
  163. raise GException(e)
  164. if self.canvas:
  165. win = self.canvas.parent
  166. if gxmXml.pos:
  167. win.SetPosition(gxmXml.pos)
  168. if gxmXml.size:
  169. win.SetSize(gxmXml.size)
  170. # load properties
  171. self.properties = gxmXml.properties
  172. self.variables = gxmXml.variables
  173. # load model.GetActions()
  174. for action in gxmXml.actions:
  175. actionItem = ModelAction(parent = self,
  176. x = action['pos'][0],
  177. y = action['pos'][1],
  178. width = action['size'][0],
  179. height = action['size'][1],
  180. task = action['task'],
  181. id = action['id'])
  182. if action['disabled']:
  183. actionItem.Enable(False)
  184. self.AddItem(actionItem)
  185. actionItem.SetValid(actionItem.GetTask().get_options())
  186. actionItem.GetLog() # substitute variables (-> valid/invalid)
  187. # load data & relations
  188. for data in gxmXml.data:
  189. dataItem = ModelData(parent = self,
  190. x = data['pos'][0],
  191. y = data['pos'][1],
  192. width = data['size'][0],
  193. height = data['size'][1],
  194. prompt = data['prompt'],
  195. value = data['value'])
  196. dataItem.SetIntermediate(data['intermediate'])
  197. for rel in data['rels']:
  198. actionItem = self.FindAction(rel['id'])
  199. if rel['dir'] == 'from':
  200. relation = ModelRelation(parent = self, fromShape = dataItem,
  201. toShape = actionItem, param = rel['name'])
  202. else:
  203. relation = ModelRelation(parent = self, fromShape = actionItem,
  204. toShape = dataItem, param = rel['name'])
  205. relation.SetControlPoints(rel['points'])
  206. actionItem.AddRelation(relation)
  207. dataItem.AddRelation(relation)
  208. if self.canvas:
  209. dataItem.Update()
  210. # load loops
  211. for loop in gxmXml.loops:
  212. loopItem = ModelLoop(parent = self,
  213. x = loop['pos'][0],
  214. y = loop['pos'][1],
  215. width = loop['size'][0],
  216. height = loop['size'][1],
  217. text = loop['text'],
  218. id = loop['id'])
  219. self.AddItem(loopItem)
  220. # load conditions
  221. for condition in gxmXml.conditions:
  222. conditionItem = ModelCondition(parent = self,
  223. x = condition['pos'][0],
  224. y = condition['pos'][1],
  225. width = condition['size'][0],
  226. height = condition['size'][1],
  227. text = condition['text'],
  228. id = condition['id'])
  229. self.AddItem(conditionItem)
  230. # define loops & if/else items
  231. for loop in gxmXml.loops:
  232. alist = list()
  233. for aId in loop['items']:
  234. action = self.GetItem(aId)
  235. alist.append(action)
  236. loopItem = self.GetItem(loop['id'])
  237. loopItem.SetItems(alist)
  238. for action in loopItem.GetItems():
  239. action.SetBlock(loopItem)
  240. for condition in gxmXml.conditions:
  241. conditionItem = self.GetItem(condition['id'])
  242. for b in condition['items'].keys():
  243. alist = list()
  244. for aId in condition['items'][b]:
  245. action = self.GetItem(aId)
  246. alist.append(action)
  247. conditionItem.SetItems(alist, branch = b)
  248. items = conditionItem.GetItems()
  249. for b in items.keys():
  250. for action in items[b]:
  251. action.SetBlock(conditionItem)
  252. def AddItem(self, newItem):
  253. """!Add item to the list"""
  254. iId = newItem.GetId()
  255. i = 0
  256. for item in self.items:
  257. if item.GetId() > iId:
  258. self.items.insert(i, newItem)
  259. return
  260. i += 1
  261. self.items.append(newItem)
  262. def IsValid(self):
  263. """Return True if model is valid"""
  264. if self.Validate():
  265. return False
  266. return True
  267. def Validate(self):
  268. """!Validate model, return None if model is valid otherwise
  269. error string"""
  270. errList = list()
  271. variables = self.GetVariables().keys()
  272. pattern = re.compile(r'(.*)(%.+\s?)(.*)')
  273. for action in self.GetItems(objType = ModelAction):
  274. cmd = action.GetLog(string = False)
  275. task = menuform.GUI(show = None).ParseCommand(cmd = cmd)
  276. errList += map(lambda x: cmd[0] + ': ' + x, task.get_cmd_error())
  277. # check also variables
  278. for opt in cmd[1:]:
  279. if '=' not in opt:
  280. continue
  281. key, value = opt.split('=', 1)
  282. sval = pattern.search(value)
  283. if sval:
  284. var = sval.group(2).strip()[1:] # ignore '%'
  285. if var not in variables:
  286. report = True
  287. for item in filter(lambda x: isinstance(x, ModelLoop), action.GetBlock()):
  288. if var in item.GetText():
  289. report = False
  290. break
  291. if report:
  292. errList.append(_("%s: undefined variable '%s'") % (cmd[0], var))
  293. ### TODO: check variables in file only optionally
  294. ### errList += self._substituteFile(action, checkOnly = True)
  295. return errList
  296. def _substituteFile(self, item, params = None, checkOnly = False):
  297. """!Subsitute variables in command file inputs
  298. @param checkOnly tuble - True to check variable, don't touch files
  299. @return list of undefined variables
  300. """
  301. errList = list()
  302. self.fileInput = dict()
  303. # collect ascii inputs
  304. for p in item.GetParams()['params']:
  305. if p.get('element', '') == 'file' and \
  306. p.get('prompt', '') == 'input' and \
  307. p.get('age', '') == 'old':
  308. filename = p.get('value', p.get('default', ''))
  309. if filename and \
  310. mimetypes.guess_type(filename)[0] == 'text/plain':
  311. self.fileInput[filename] = None
  312. for finput in self.fileInput:
  313. # read lines
  314. fd = open(finput, "r")
  315. try:
  316. data = self.fileInput[finput] = fd.read()
  317. finally:
  318. fd.close()
  319. # substitute variables
  320. write = False
  321. variables = self.GetVariables()
  322. for variable in variables:
  323. pattern = re.compile('%' + variable)
  324. value = ''
  325. if params and 'variables' in params:
  326. for p in params['variables']['params']:
  327. if variable == p.get('name', ''):
  328. if p.get('type', 'string') == 'string':
  329. value = p.get('value', '')
  330. else:
  331. value = str(p.get('value', ''))
  332. break
  333. if not value:
  334. value = variables[variable].get('value', '')
  335. data = pattern.sub(value, data)
  336. if not checkOnly:
  337. write = True
  338. pattern = re.compile(r'(.*)(%.+\s?)(.*)')
  339. sval = pattern.search(data)
  340. if sval:
  341. var = sval.group(2).strip()[1:] # ignore '%'
  342. cmd = item.GetLog(string = False)[0]
  343. errList.append(_("%s: undefined variable '%s'") % (cmd, var))
  344. if not checkOnly:
  345. if write:
  346. fd = open(finput, "w")
  347. try:
  348. fd.write(data)
  349. finally:
  350. fd.close()
  351. else:
  352. self.fileInput[finput] = None
  353. return errList
  354. def OnPrepare(self, item, params):
  355. self._substituteFile(item, params, checkOnly = False)
  356. def RunAction(self, item, params, log, onDone, onPrepare = None, statusbar = None):
  357. """!Run given action
  358. @param item action item
  359. @param params parameters dict
  360. @param log logging window
  361. @param onDone on-done method
  362. @param onPrepare on-prepare method
  363. @param statusbar wx.StatusBar instance or None
  364. """
  365. name = item.GetName()
  366. if name in params:
  367. paramsOrig = item.GetParams(dcopy = True)
  368. item.MergeParams(params[name])
  369. if statusbar:
  370. statusbar.SetStatusText(_('Running model...'), 0)
  371. data = { 'item' : item,
  372. 'params' : copy.deepcopy(params) }
  373. log.RunCmd(command = item.GetLog(string = False, substitute = params),
  374. onDone = onDone, onPrepare = self.OnPrepare, userData = data)
  375. if name in params:
  376. item.SetParams(paramsOrig)
  377. def Run(self, log, onDone, parent = None):
  378. """!Run model
  379. @param log logging window (see goutput.GMConsole)
  380. @param onDone on-done method
  381. @param parent window for messages or None
  382. """
  383. if self.GetNumItems() < 1:
  384. GMessage(parent = parent,
  385. message = _('Model is empty. Nothing to run.'))
  386. return
  387. statusbar = None
  388. if isinstance(parent, wx.Frame):
  389. statusbar = parent.GetStatusBar()
  390. # validation
  391. if statusbar:
  392. statusbar.SetStatusText(_('Validating model...'), 0)
  393. errList = self.Validate()
  394. if statusbar:
  395. statusbar.SetStatusText('', 0)
  396. if errList:
  397. dlg = wx.MessageDialog(parent = parent,
  398. message = _('Model is not valid. Do you want to '
  399. 'run the model anyway?\n\n%s') % '\n'.join(errList),
  400. caption = _("Run model?"),
  401. style = wx.YES_NO | wx.NO_DEFAULT |
  402. wx.ICON_QUESTION | wx.CENTRE)
  403. ret = dlg.ShowModal()
  404. if ret != wx.ID_YES:
  405. return
  406. # parametrization
  407. params = self.Parameterize()
  408. if params:
  409. dlg = ModelParamDialog(parent = parent,
  410. params = params)
  411. dlg.CenterOnParent()
  412. ret = dlg.ShowModal()
  413. if ret != wx.ID_OK:
  414. dlg.Destroy()
  415. return
  416. err = dlg.GetErrors()
  417. if err:
  418. GError(parent = parent, message = unicode('\n'.join(err)))
  419. return
  420. err = list()
  421. for key, item in params.iteritems():
  422. for p in item['params']:
  423. if p.get('value', '') == '':
  424. err.append((key, p.get('name', ''), p.get('description', '')))
  425. if err:
  426. GError(parent = parent,
  427. message = _("Variables below not defined:") + \
  428. "\n\n" + unicode('\n'.join(map(lambda x: "%s: %s (%s)" % (x[0], x[1], x[2]), err))))
  429. return
  430. log.cmdThread.SetId(-1)
  431. for item in self.GetItems():
  432. if not item.IsEnabled():
  433. continue
  434. if isinstance(item, ModelAction):
  435. if item.GetBlockId():
  436. continue
  437. self.RunAction(item, params, log, onDone)
  438. elif isinstance(item, ModelLoop):
  439. cond = item.GetText()
  440. # substitute variables in condition
  441. variables = self.GetVariables()
  442. for variable in variables:
  443. pattern = re.compile('%' + variable)
  444. if pattern.search(cond):
  445. value = ''
  446. if params and 'variables' in params:
  447. for p in params['variables']['params']:
  448. if variable == p.get('name', ''):
  449. value = p.get('value', '')
  450. break
  451. if not value:
  452. value = variables[variable].get('value', '')
  453. if not value:
  454. continue
  455. vtype = variables[variable].get('type', 'string')
  456. if vtype == 'string':
  457. value = '"' + value + '"'
  458. cond = pattern.sub(value, cond)
  459. # split condition
  460. condVar, condText = map(lambda x: x.strip(), re.split('\s*in\s*', cond))
  461. pattern = re.compile('%' + condVar)
  462. ### for vars()[condVar] in eval(condText): ?
  463. if condText[0] == '`' and condText[-1] == '`':
  464. # run command
  465. cmd, dcmd = utils.CmdToTuple(condText[1:-1].split(' '))
  466. ret = RunCommand(cmd,
  467. read = True,
  468. **dcmd)
  469. if ret:
  470. vlist = ret.splitlines()
  471. else:
  472. vlist = eval(condText)
  473. if 'variables' not in params:
  474. params['variables'] = { 'params' : [] }
  475. varDict = { 'name' : condVar, 'value' : '' }
  476. params['variables']['params'].append(varDict)
  477. for var in vlist:
  478. for action in item.GetItems():
  479. if not isinstance(action, ModelAction) or \
  480. not action.IsEnabled():
  481. continue
  482. varDict['value'] = var
  483. self.RunAction(item = action, params = params,
  484. log = log, onDone = onDone)
  485. params['variables']['params'].remove(varDict)
  486. # discard values
  487. if params:
  488. for item in params.itervalues():
  489. for p in item['params']:
  490. p['value'] = ''
  491. if params:
  492. dlg.Destroy()
  493. def DeleteIntermediateData(self, log):
  494. """!Detele intermediate data"""
  495. rast, vect, rast3d, msg = self.GetIntermediateData()
  496. if rast:
  497. log.RunCmd(['g.remove', 'rast=%s' %','.join(rast)])
  498. if rast3d:
  499. log.RunCmd(['g.remove', 'rast3d=%s' %','.join(rast3d)])
  500. if vect:
  501. log.RunCmd(['g.remove', 'vect=%s' %','.join(vect)])
  502. def GetIntermediateData(self):
  503. """!Get info about intermediate data"""
  504. rast = list()
  505. rast3d = list()
  506. vect = list()
  507. for data in self.GetData():
  508. if not data.IsIntermediate():
  509. continue
  510. name = data.GetValue()
  511. prompt = data.GetPrompt()
  512. if prompt == 'raster':
  513. rast.append(name)
  514. elif prompt == 'vector':
  515. vect.append(name)
  516. elif prompt == 'rast3d':
  517. rast3d.append(name)
  518. msg = ''
  519. if rast:
  520. msg += '\n\n%s: ' % _('Raster maps')
  521. msg += ', '.join(rast)
  522. if rast3d:
  523. msg += '\n\n%s: ' % _('3D raster maps')
  524. msg += ', '.join(rast3d)
  525. if vect:
  526. msg += '\n\n%s: ' % _('Vector maps')
  527. msg += ', '.join(vect)
  528. return rast, vect, rast3d, msg
  529. def Update(self):
  530. """!Update model"""
  531. for item in self.items:
  532. item.Update()
  533. def IsParameterized(self):
  534. """!Return True if model is parameterized"""
  535. if self.Parameterize():
  536. return True
  537. return False
  538. def Parameterize(self):
  539. """!Return parameterized options"""
  540. result = dict()
  541. idx = 0
  542. if self.variables:
  543. params = list()
  544. result["variables"] = { 'flags' : list(),
  545. 'params' : params,
  546. 'idx' : idx }
  547. for name, values in self.variables.iteritems():
  548. gtype = values.get('type', 'string')
  549. if gtype in ('raster', 'vector', 'mapset', 'file'):
  550. gisprompt = True
  551. prompt = gtype
  552. if gtype == 'raster':
  553. element = 'cell'
  554. else:
  555. element = gtype
  556. ptype = 'string'
  557. else:
  558. gisprompt = False
  559. prompt = None
  560. element = None
  561. ptype = gtype
  562. params.append({ 'gisprompt' : gisprompt,
  563. 'multiple' : False,
  564. 'description' : values.get('description', ''),
  565. 'guidependency' : '',
  566. 'default' : '',
  567. 'age' : None,
  568. 'required' : True,
  569. 'value' : values.get('value', ''),
  570. 'label' : '',
  571. 'guisection' : '',
  572. 'key_desc' : '',
  573. 'values' : list(),
  574. 'parameterized' : False,
  575. 'values_desc' : list(),
  576. 'prompt' : prompt,
  577. 'element' : element,
  578. 'type' : ptype,
  579. 'name' : name })
  580. idx += 1
  581. for action in self.GetItems(objType = ModelAction):
  582. if not action.IsEnabled():
  583. continue
  584. name = action.GetName()
  585. params = action.GetParams()
  586. for f in params['flags']:
  587. if f.get('parameterized', False):
  588. if name not in result:
  589. result[name] = { 'flags' : list(),
  590. 'params': list(),
  591. 'idx' : idx }
  592. result[name]['flags'].append(f)
  593. for p in params['params']:
  594. if p.get('parameterized', False):
  595. if name not in result:
  596. result[name] = { 'flags' : list(),
  597. 'params': list(),
  598. 'idx' : idx }
  599. result[name]['params'].append(p)
  600. if name in result:
  601. idx += 1
  602. self.variablesParams = result # record parameters
  603. return result