gselect.py 88 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341
  1. """
  2. @package gui_core.gselect
  3. @brief Custom control that selects elements
  4. Classes:
  5. - :class:`Select`
  6. - :class:`VectorSelect`
  7. - :class:`ListCtrlComboPopup`
  8. - :class:`TreeCrtlComboPopup`
  9. - :class:`VectorDBInfo`
  10. - :class:`LayerSelect`
  11. - :class:`DriverSelect`
  12. - :class:`DatabaseSelect`
  13. - :class:`TableSelect`
  14. - :class:`ColumnSelect`
  15. - :class:`DbaseSelect`
  16. - :class:`LocationSelect`
  17. - :class:`MapsetSelect`
  18. - :class:`SubGroupSelect`
  19. - :class:`FormatSelect`
  20. - :class:`GdalSelect`
  21. - :class:`ProjSelect`
  22. - :class:`ElementSelect`
  23. - :class:`OgrTypeSelect`
  24. - :class:`CoordinatesSelect`
  25. - :class:`SignatureSelect`
  26. - :class:`SeparatorSelect`
  27. (C) 2007-2014 by the GRASS Development Team
  28. This program is free software under the GNU General Public License
  29. (>=v2). Read the file COPYING that comes with GRASS for details.
  30. @author Michael Barton
  31. @author Martin Landa <landa.martin gmail.com>
  32. @author Vaclav Petras <wenzeslaus gmail.com> (menu customization)
  33. @author Stepan Turek <stepan.turek seznam.cz> (CoordinatesSelect, ListCtrlComboPopup)
  34. """
  35. import os
  36. import sys
  37. import glob
  38. import copy
  39. import wx
  40. import wx.combo
  41. import wx.lib.buttons as buttons
  42. import wx.lib.filebrowsebutton as filebrowse
  43. from core import globalvar
  44. import grass.script as grass
  45. from grass.script import task as gtask
  46. from grass.exceptions import CalledModuleError
  47. try:
  48. from grass.pygrass import messages
  49. except ImportError as e:
  50. print >> sys.stderr, _("Unable to import pyGRASS: %s\n"
  51. "Some functionality will be not accessible") % e
  52. from gui_core.widgets import ManageSettingsWidget, CoordinatesValidator
  53. from core.gcmd import RunCommand, GError, GMessage
  54. from core.utils import GetListOfLocations, GetListOfMapsets, \
  55. GetFormats, rasterFormatExtension, vectorFormatExtension
  56. from core.utils import GetSettingsPath, GetValidLayerName, ListSortLower
  57. from core.utils import GetVectorNumberOfLayers, _
  58. from core.settings import UserSettings
  59. from core.debug import Debug
  60. from grass.pydispatch.signal import Signal
  61. class Select(wx.combo.ComboCtrl):
  62. def __init__(self, parent, id = wx.ID_ANY, size = globalvar.DIALOG_GSELECT_SIZE,
  63. type = None, multiple = False, nmaps = 1,
  64. mapsets = None, updateOnPopup = True, onPopup = None,
  65. fullyQualified = True, extraItems = {}, layerTree = None,
  66. validator = wx.DefaultValidator):
  67. """Custom control to create a ComboBox with a tree control to
  68. display and select GIS elements within acessible mapsets.
  69. Elements can be selected with mouse. Can allow multiple
  70. selections, when argument <em>multiple</em> is True. Multiple
  71. selections are separated by commas.
  72. :param type: type of GIS elements ('raster, 'vector', ...)
  73. :param multiple: True for multiple input
  74. :param nmaps: number of maps to be entered
  75. :param mapsets: force list of mapsets (otherwise search path)
  76. :param updateOnPopup: True for updating list of elements on popup
  77. :param onPopup: function to be called on Popup
  78. :param fullyQualified: True to provide fully qualified names (map@mapset)
  79. :param extraItems: extra items to add (given as dictionary) - see gmodeler for usage
  80. :param layerTree: show only elements from given layer tree if not None
  81. :param validator: validator for TextCtrl
  82. """
  83. wx.combo.ComboCtrl.__init__(self, parent=parent, id=id, size=size, validator=validator)
  84. if globalvar.CheckWxVersion([3]):
  85. self.SetName("Select")
  86. else:
  87. self.GetChildren()[0].SetName("Select")
  88. self.GetChildren()[0].type = type
  89. self.tcp = TreeCtrlComboPopup()
  90. self.SetPopupControl(self.tcp)
  91. self.SetPopupExtents(0, 100)
  92. if type:
  93. self.tcp.SetData(type = type, mapsets = mapsets,
  94. multiple = multiple, nmaps = nmaps,
  95. updateOnPopup = updateOnPopup, onPopup = onPopup,
  96. fullyQualified = fullyQualified, extraItems = extraItems, layerTree = layerTree)
  97. self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
  98. def OnKeyDown(self, event):
  99. """Open popup and send key events to the tree."""
  100. if self.IsPopupShown():
  101. # on some configurations we get key down, with some only key up
  102. # so we are trying to catch either key up or down
  103. # this mess shouldn't be necessary with wxPython 3
  104. self.tcp.OnKeyUp(event)
  105. else:
  106. if event.GetKeyCode() == wx.WXK_DOWN:
  107. self.ShowPopup()
  108. event.Skip()
  109. def SetElementList(self, type, mapsets = None):
  110. """Set element list
  111. :param type: GIS element type
  112. :param mapsets: list of acceptable mapsets (None for all in search path)
  113. """
  114. self.tcp.SetData(type = type, mapsets = mapsets)
  115. def GetElementList(self):
  116. """Load elements"""
  117. self.tcp.GetElementList()
  118. def SetType(self, etype, multiple = False, nmaps = 1,
  119. mapsets = None, updateOnPopup = True, onPopup = None):
  120. """Param set element type for widget
  121. :param etype: element type, see gselect.ElementSelect
  122. """
  123. self.tcp.SetData(type = etype, mapsets = mapsets,
  124. multiple = multiple, nmaps = nmaps,
  125. updateOnPopup = updateOnPopup, onPopup = onPopup)
  126. class VectorSelect(Select):
  127. def __init__(self, parent, ftype, **kwargs):
  128. """Custom to create a ComboBox with a tree control to display and
  129. select vector maps. You can filter the vector maps. If you
  130. don't need this feature use Select class instead
  131. :param ftype: filter vector maps based on feature type
  132. """
  133. Select.__init__(self, parent = parent, id = wx.ID_ANY,
  134. type = 'vector', **kwargs)
  135. self.ftype = ftype
  136. # remove vector maps which do not contain given feature type
  137. self.tcp.SetFilter(self._isElement)
  138. def _isElement(self, vectorName):
  139. """Check if element should be filtered out"""
  140. try:
  141. if int(grass.vector_info_topo(vectorName)[self.ftype]) < 1:
  142. return False
  143. except KeyError:
  144. return False
  145. return True
  146. class ListCtrlComboPopup(wx.combo.ComboPopup):
  147. """Create a list ComboBox using TreeCtrl with hidden root.
  148. .. todo::
  149. use event.EventObject instead of hardcoding (see forms.py)
  150. https://groups.google.com/forum/#!topic/wxpython-users/pRz6bi0k0XY
  151. """
  152. # overridden ComboPopup methods
  153. def Init(self):
  154. self.value = [] # for multiple is False ->
  155. # len(self.value) in [0,1]
  156. self.curitem = None
  157. self.multiple = False
  158. self.updateOnPopup = True
  159. self.filterItems = [] # limit items based on this list,
  160. # see layerTree parameter
  161. def Create(self, parent):
  162. self.seltree = wx.TreeCtrl(parent, style=wx.TR_HIDE_ROOT
  163. |wx.TR_HAS_BUTTONS
  164. |wx.TR_SINGLE
  165. |wx.TR_LINES_AT_ROOT
  166. |wx.SIMPLE_BORDER
  167. |wx.TR_FULL_ROW_HIGHLIGHT)
  168. self.seltree.Bind(wx.EVT_MOTION, self.OnMotion)
  169. self.seltree.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
  170. # the following dummy handler are needed to keep tree events
  171. # from propagating up to the parent GIS Manager layer tree
  172. self.seltree.Bind(wx.EVT_TREE_ITEM_EXPANDING, lambda x: None)
  173. self.seltree.Bind(wx.EVT_TREE_ITEM_COLLAPSED, lambda x: None)
  174. self.seltree.Bind(wx.EVT_TREE_SEL_CHANGED, lambda x: None)
  175. self.seltree.Bind(wx.EVT_TREE_DELETE_ITEM, lambda x: None)
  176. self.seltree.Bind(wx.EVT_TREE_BEGIN_DRAG, lambda x: None)
  177. self.seltree.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, lambda x: None)
  178. # navigation in list/tree is handled automatically since wxPython 3
  179. # for older versions, we have to workaround it and write our own navigation
  180. if globalvar.CheckWxVersion(version=[3]):
  181. self.seltree.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self._onItemConfirmed)
  182. self.seltree.Bind(wx.EVT_TREE_KEY_DOWN, self._onDismissPopup)
  183. else:
  184. self.seltree.Bind(wx.EVT_TREE_ITEM_ACTIVATED, lambda x: None)
  185. self.seltree.Bind(wx.EVT_KEY_UP, self.OnKeyUp)
  186. def GetControl(self):
  187. return self.seltree
  188. def GetStringValue(self):
  189. """Get value as a string separated by commas
  190. """
  191. return ','.join(self.value)
  192. def SetStringValue(self, value):
  193. # this assumes that item strings are unique...
  194. root = self.seltree.GetRootItem()
  195. if not root:
  196. return
  197. winValue = self.GetCombo().GetValue().strip(',')
  198. self.value = []
  199. if winValue:
  200. self.value = winValue.split(',')
  201. def OnPopup(self, force = False):
  202. """Limited only for first selected
  203. """
  204. if not force and not self.updateOnPopup:
  205. return
  206. # selects map starting according to written text
  207. inputText = self.GetCombo().GetValue().strip()
  208. if inputText:
  209. root = self.seltree.GetRootItem()
  210. match = self.FindItem(root, inputText, startLetters = True)
  211. if match.IsOk():
  212. self.seltree.EnsureVisible(match)
  213. self.seltree.SelectItem(match)
  214. def GetAdjustedSize(self, minWidth, prefHeight, maxHeight):
  215. """Reads UserSettings to get height (which was 200 in old implementation).
  216. """
  217. height = UserSettings.Get(group = 'appearance', key = 'gSelectPopupHeight', subkey = 'value')
  218. return wx.Size(minWidth, min(height, maxHeight))
  219. def FindItem(self, parentItem, text, startLetters = False):
  220. """Finds item with given name or starting with given text
  221. """
  222. startletters = startLetters
  223. item, cookie = self.seltree.GetFirstChild(parentItem)
  224. while wx.TreeItemId.IsOk(item):
  225. if self.seltree.GetItemText(item) == text:
  226. return item
  227. if self.seltree.ItemHasChildren(item):
  228. item = self.FindItem(item, text, startLetters = startletters)
  229. if wx.TreeItemId.IsOk(item):
  230. return item
  231. elif startletters and self.seltree.GetItemText(item).startswith(text.split('@', 1)[0]):
  232. return item
  233. item, cookie = self.seltree.GetNextChild(parentItem, cookie)
  234. return wx.TreeItemId()
  235. def AddItem(self, value):
  236. root = self.seltree.GetRootItem()
  237. if not root:
  238. root = self.seltree.AddRoot("<hidden root>")
  239. self.seltree.AppendItem(root, text = value)
  240. def SetItems(self, items):
  241. root = self.seltree.GetRootItem()
  242. if not root:
  243. root = self.seltree.AddRoot("<hidden root>")
  244. for item in items:
  245. self.seltree.AppendItem(root, text = item)
  246. def OnKeyUp(self, event):
  247. """Enable to select items using keyboard.
  248. Unused with wxPython 3, can be removed in the future.
  249. """
  250. item = self.seltree.GetSelection()
  251. if event.GetKeyCode() == wx.WXK_DOWN:
  252. self.seltree.SelectItem(self.seltree.GetNextVisible(item))
  253. elif event.GetKeyCode() == wx.WXK_UP:
  254. itemPrev = self.seltree.GetPrevSibling(item)
  255. self.seltree.SelectItem(itemPrev)
  256. elif event.GetKeyCode() == wx.WXK_ESCAPE:
  257. self.Dismiss()
  258. elif event.GetKeyCode() == wx.WXK_RETURN:
  259. self.seltree.SelectItem(item)
  260. self.curitem = item
  261. self._selectTreeItem(item)
  262. self.Dismiss()
  263. def _onDismissPopup(self, event):
  264. """Hide popup without selecting item on Esc"""
  265. if event.GetKeyCode() == wx.WXK_ESCAPE:
  266. self.Dismiss()
  267. else:
  268. event.Skip()
  269. def _selectTreeItem(self, item):
  270. item_str = self.seltree.GetItemText(item)
  271. if self.multiple:
  272. if item_str not in self.value:
  273. self.value.append(item_str)
  274. else:
  275. self.value = [item_str]
  276. def _onItemConfirmed(self, event):
  277. item = event.GetItem()
  278. self._selectTreeItem(item)
  279. self.Dismiss()
  280. def OnMotion(self, evt):
  281. """Have the selection follow the mouse, like in a real combobox
  282. """
  283. item, flags = self.seltree.HitTest(evt.GetPosition())
  284. if item and flags & wx.TREE_HITTEST_ONITEMLABEL:
  285. self.seltree.SelectItem(item)
  286. self.curitem = item
  287. evt.Skip()
  288. def OnLeftDown(self, evt):
  289. """Do the combobox selection
  290. """
  291. if self.curitem is None:
  292. return
  293. self._selectTreeItem(self.curitem)
  294. self.Dismiss()
  295. evt.Skip()
  296. def SetData(self, **kargs):
  297. """Set object properties"""
  298. if 'multiple' in kargs:
  299. self.multiple = kargs['multiple']
  300. if 'onPopup' in kargs:
  301. self.onPopup = kargs['onPopup']
  302. if kargs.get('layerTree', None):
  303. self.filterItems = [] # reset
  304. ltype = kargs['type']
  305. for layer in kargs['layerTree'].GetVisibleLayers(skipDigitized = True):
  306. if layer.GetType() != ltype:
  307. continue
  308. self.filterItems.append(layer.GetName())
  309. def DeleteAllItems(self):
  310. """Delete all items in popup"""
  311. self.seltree.DeleteAllItems()
  312. class TreeCtrlComboPopup(ListCtrlComboPopup):
  313. """Create a tree ComboBox for selecting maps and other GIS elements
  314. in accessible mapsets within the current location
  315. """
  316. # overridden ComboPopup methods
  317. def Init(self):
  318. ListCtrlComboPopup.Init(self)
  319. self.nmaps = 1
  320. self.type = None
  321. self.mapsets = None
  322. self.onPopup = None
  323. self.fullyQualified = True
  324. self.extraItems = dict()
  325. self.SetFilter(None)
  326. self.tgis_error = False
  327. def SetFilter(self, filter):
  328. """Set filter for GIS elements, see e.g. VectorSelect"""
  329. self.filterElements = filter
  330. def OnPopup(self, force = False):
  331. """Limited only for first selected"""
  332. if not force and not self.updateOnPopup:
  333. return
  334. if self.onPopup:
  335. selected, exclude = self.onPopup(self.type)
  336. else:
  337. selected = None
  338. exclude = False
  339. self.GetElementList(selected, exclude)
  340. ListCtrlComboPopup.OnPopup(self, force)
  341. def GetElementList(self, elements = None, exclude = False):
  342. """Get filtered list of GIS elements in accessible mapsets
  343. and display as tree with all relevant elements displayed
  344. beneath each mapset branch
  345. """
  346. # update list
  347. self.seltree.DeleteAllItems()
  348. if self.type:
  349. self._getElementList(self.type, self.mapsets, elements, exclude)
  350. if len(self.value) > 0:
  351. root = self.seltree.GetRootItem()
  352. if not root:
  353. return
  354. item = self.FindItem(root, self.value[0])
  355. try:
  356. self.seltree.EnsureVisible(item)
  357. self.seltree.SelectItem(item)
  358. except:
  359. pass
  360. def _getElementList(self, element, mapsets = None, elements = None, exclude = False):
  361. """Get list of GIS elements in accessible mapsets and display as tree
  362. with all relevant elements displayed beneath each mapset branch
  363. :param element: GIS element
  364. :param mapsets: list of acceptable mapsets (None for all mapsets in search path)
  365. :param elements: list of forced GIS elements
  366. :param exclude: True to exclude, False for forcing the list (elements)
  367. """
  368. # get current mapset
  369. curr_mapset = grass.gisenv()['MAPSET']
  370. # map element types to g.list types
  371. elementdict = {'cell': 'raster',
  372. 'raster': 'raster',
  373. 'grid3': 'raster_3d',
  374. 'raster_3d': 'raster_3d',
  375. 'vector': 'vector',
  376. 'paint/labels': 'label',
  377. 'label': 'label',
  378. 'windows': 'region',
  379. 'region': 'region',
  380. 'group': 'group',
  381. 'stds': 'stds',
  382. 'strds': 'strds',
  383. 'str3ds': 'str3ds',
  384. 'stvds': 'stvds'}
  385. # to support multiple elements
  386. element_list = element.split(',')
  387. renamed_elements = []
  388. for elem in element_list:
  389. if elem not in elementdict:
  390. self.AddItem(_('Not selectable element'), node = False)
  391. return
  392. else:
  393. renamed_elements.append(elementdict[elem])
  394. if element in ('stds', 'strds', 'str3ds', 'stvds'):
  395. if self.tgis_error is False:
  396. import grass.temporal as tgis
  397. filesdict = tgis.tlist_grouped(elementdict[element], element == 'stds')
  398. else:
  399. filesdict = None
  400. else:
  401. filesdict = grass.list_grouped(renamed_elements,
  402. check_search_path=False)
  403. # add extra items first
  404. if self.extraItems:
  405. for group, items in self.extraItems.iteritems():
  406. node = self.AddItem(group, node = True)
  407. self.seltree.SetItemTextColour(node, wx.Colour(50, 50, 200))
  408. for item in items:
  409. self.AddItem(item, node = False, parent = node)
  410. self.seltree.ExpandAllChildren(node)
  411. # list of mapsets in current location
  412. if mapsets is None:
  413. mapsets = grass.mapsets(search_path = True)
  414. # current mapset first
  415. if curr_mapset in mapsets and mapsets[0] != curr_mapset:
  416. mapsets.remove(curr_mapset)
  417. mapsets.insert(0, curr_mapset)
  418. first_mapset = None
  419. for mapset in mapsets:
  420. mapset_node = self.AddItem(_('Mapset') + ': ' + mapset, node = True, mapset = mapset)
  421. node = mapset_node
  422. if not first_mapset:
  423. first_mapset = mapset_node
  424. self.seltree.SetItemTextColour(mapset_node, wx.Colour(50, 50, 200))
  425. if mapset not in filesdict:
  426. continue
  427. try:
  428. if type(filesdict[mapset]) == dict:
  429. for elementType in filesdict[mapset].keys():
  430. node = self.AddItem(_('Type: ') + elementType, mapset = mapset,
  431. node = True, parent = mapset_node)
  432. self.seltree.SetItemTextColour(node, wx.Colour(50, 50, 200))
  433. elem_list = filesdict[mapset][elementType]
  434. self._addItems(elist = elem_list, elements = elements,
  435. mapset = mapset, exclude = exclude, node = node)
  436. else:
  437. elem_list = filesdict[mapset]
  438. self._addItems(elist = elem_list, elements = elements,
  439. mapset = mapset, exclude = exclude, node = node)
  440. except StandardError as e:
  441. sys.stderr.write(_("GSelect: invalid item: %s") % e)
  442. continue
  443. if self.seltree.ItemHasChildren(mapset_node):
  444. sel = UserSettings.Get(group='appearance', key='elementListExpand',
  445. subkey='selection')
  446. collapse = True
  447. if sel == 0: # collapse all except PERMANENT and current
  448. if mapset in ('PERMANENT', curr_mapset):
  449. collapse = False
  450. elif sel == 1: # collapse all except PERMANENT
  451. if mapset == 'PERMANENT':
  452. collapse = False
  453. elif sel == 2: # collapse all except current
  454. if mapset == curr_mapset:
  455. collapse = False
  456. elif sel == 3: # collapse all
  457. pass
  458. elif sel == 4: # expand all
  459. collapse = False
  460. if collapse:
  461. self.seltree.CollapseAllChildren(mapset_node)
  462. else:
  463. self.seltree.ExpandAllChildren(mapset_node)
  464. if first_mapset:
  465. # select first mapset (MSW hack)
  466. self.seltree.SelectItem(first_mapset)
  467. # helpers
  468. def _addItems(self, elist, elements, mapset, exclude, node):
  469. """Helper function for adding multiple items (maps, stds).
  470. :param list elist: list of map/stds names
  471. :param list elements: list of forced elements
  472. :param str mapset: mapset name
  473. :param exclude: True to exclude, False for forcing the list
  474. :param node: parent node
  475. """
  476. elist = grass.natural_sort(elist)
  477. for elem in elist:
  478. if elem != '':
  479. fullqElem = elem + '@' + mapset
  480. if self.filterItems and fullqElem not in self.filterItems:
  481. continue # skip items missed in self.filterItems
  482. if elements is not None:
  483. if (exclude and fullqElem in elements) or \
  484. (not exclude and fullqElem not in elements):
  485. continue
  486. if self.filterElements:
  487. if self.filterElements(fullqElem):
  488. self.AddItem(elem, mapset = mapset, node = False, parent = node)
  489. else:
  490. self.AddItem(elem, mapset = mapset, node = False, parent = node)
  491. def AddItem(self, value, mapset = None, node = True, parent = None):
  492. if not parent:
  493. root = self.seltree.GetRootItem()
  494. if not root:
  495. root = self.seltree.AddRoot("<hidden root>")
  496. parent = root
  497. data = {'node': node, 'mapset': mapset}
  498. item = self.seltree.AppendItem(parent, text = value, data = wx.TreeItemData(data))
  499. return item
  500. def OnKeyUp(self, event):
  501. """Enables to select items using keyboard
  502. Unused with wxPython 3, can be removed in the future.
  503. """
  504. item = self.seltree.GetSelection()
  505. if event.GetKeyCode() == wx.WXK_DOWN:
  506. self.seltree.SelectItem(self.seltree.GetNextVisible(item))
  507. # problem with GetPrevVisible
  508. elif event.GetKeyCode() == wx.WXK_UP:
  509. if self.seltree.ItemHasChildren(item) and self.seltree.IsExpanded(self.seltree.GetPrevSibling(item)):
  510. itemPrev = self.seltree.GetLastChild(self.seltree.GetPrevSibling(item))
  511. else:
  512. itemPrev = self.seltree.GetPrevSibling(item)
  513. if not wx.TreeItemId.IsOk(itemPrev):
  514. itemPrev = self.seltree.GetItemParent(item)
  515. if item == self.seltree.GetFirstChild(self.seltree.GetRootItem())[0]:
  516. itemPrev = item
  517. self.seltree.SelectItem(itemPrev)
  518. # selects first item starting with the written text in next mapset
  519. elif event.GetKeyCode() == wx.WXK_TAB:
  520. selected = self.seltree.GetSelection()
  521. if self.seltree.ItemHasChildren(selected):
  522. parent = selected
  523. else:
  524. parent = self.seltree.GetItemParent(selected)
  525. nextSibling = self.seltree.GetNextSibling(parent)
  526. if wx.TreeItemId.IsOk(nextSibling):
  527. match = self.FindItem(nextSibling, self.GetCombo().GetValue().strip(), True)
  528. else:
  529. match = self.FindItem(self.seltree.GetFirstChild(self.seltree.GetItemParent(parent))[0],
  530. self.GetCombo().GetValue().strip(), True)
  531. self.seltree.SelectItem(match)
  532. elif event.GetKeyCode() == wx.WXK_RIGHT:
  533. if self.seltree.ItemHasChildren(item):
  534. self.seltree.Expand(item)
  535. elif event.GetKeyCode() == wx.WXK_LEFT:
  536. if self.seltree.ItemHasChildren(item):
  537. self.seltree.Collapse(item)
  538. elif event.GetKeyCode() == wx.WXK_ESCAPE:
  539. self.Dismiss()
  540. elif event.GetKeyCode() == wx.WXK_RETURN:
  541. if self.seltree.GetPyData(item)['node']:
  542. self.value = []
  543. else:
  544. self._selectTreeItem(item)
  545. self.Dismiss()
  546. def OnLeftDown(self, evt):
  547. """Do the combobox selection
  548. """
  549. item, flags = self.seltree.HitTest(evt.GetPosition())
  550. if item and flags & wx.TREE_HITTEST_ONITEMLABEL:
  551. self.curitem = item
  552. if self.seltree.GetPyData(item)['node']:
  553. evt.Skip()
  554. return
  555. self._selectTreeItem(item)
  556. self.Dismiss()
  557. evt.Skip()
  558. def _selectTreeItem(self, item):
  559. fullName = self.seltree.GetItemText(item)
  560. if self.fullyQualified and self.seltree.GetPyData(item)['mapset']:
  561. fullName += '@' + self.seltree.GetPyData(item)['mapset']
  562. if self.multiple:
  563. self.value.append(fullName)
  564. else:
  565. if self.nmaps > 1: # see key_desc
  566. if len(self.value) >= self.nmaps:
  567. self.value = [fullName]
  568. else:
  569. self.value.append(fullName)
  570. else:
  571. self.value = [fullName]
  572. def _onItemConfirmed(self, event):
  573. item = event.GetItem()
  574. if self.seltree.GetPyData(item)['node']:
  575. self.value = []
  576. else:
  577. self._selectTreeItem(item)
  578. self.Dismiss()
  579. def SetData(self, **kargs):
  580. """Set object properties"""
  581. ListCtrlComboPopup.SetData(self, **kargs)
  582. if 'type' in kargs:
  583. self.type = kargs['type']
  584. if self.type in ('stds', 'strds', 'str3ds', 'stvds'):
  585. # Initiate the temporal framework. Catch database error
  586. # and set the error flag for the stds listing.
  587. import grass.temporal as tgis
  588. try:
  589. tgis.init(True)
  590. except messages.FatalError as e:
  591. sys.stderr.write("Temporal GIS error:\n%s" % e)
  592. self.tgis_error = True
  593. if 'mapsets' in kargs:
  594. self.mapsets = kargs['mapsets']
  595. if 'nmaps' in kargs:
  596. self.nmaps = kargs['nmaps']
  597. if 'updateOnPopup' in kargs:
  598. self.updateOnPopup = kargs['updateOnPopup']
  599. if 'fullyQualified' in kargs:
  600. self.fullyQualified = kargs['fullyQualified']
  601. if 'extraItems' in kargs:
  602. self.extraItems = kargs['extraItems']
  603. def GetType(self):
  604. """Get element type
  605. """
  606. return self.type
  607. class VectorDBInfo:
  608. """Class providing information about attribute tables
  609. linked to a vector map"""
  610. def __init__(self, map):
  611. self.map = map
  612. # dictionary of layer number and associated (driver, database, table)
  613. self.layers = {}
  614. # dictionary of table and associated columns (type, length, values, ids)
  615. self.tables = {}
  616. if not self._CheckDBConnection(): # -> self.layers
  617. return
  618. self._DescribeTables() # -> self.tables
  619. def _CheckDBConnection(self):
  620. """Check DB connection"""
  621. nuldev = file(os.devnull, 'w+')
  622. # if map is not defined (happens with vnet initialization) or it doesn't exist
  623. try:
  624. self.layers = grass.vector_db(map=self.map, stderr=nuldev)
  625. except CalledModuleError:
  626. return False
  627. finally: # always close nuldev
  628. nuldev.close()
  629. return bool(len(self.layers.keys()) > 0)
  630. def _DescribeTables(self):
  631. """Describe linked tables"""
  632. for layer in self.layers.keys():
  633. # determine column names and types
  634. table = self.layers[layer]["table"]
  635. columns = {} # {name: {type, length, [values], [ids]}}
  636. i = 0
  637. Debug.msg(1, "gselect.VectorDBInfo._DescribeTables(): table=%s driver=%s database=%s" % \
  638. (self.layers[layer]["table"], self.layers[layer]["driver"],
  639. self.layers[layer]["database"]))
  640. for item in grass.db_describe(table = self.layers[layer]["table"],
  641. driver = self.layers[layer]["driver"],
  642. database = self.layers[layer]["database"])['cols']:
  643. name, type, length = item
  644. # FIXME: support more datatypes
  645. if type.lower() == "integer":
  646. ctype = int
  647. elif type.lower() == "double precision":
  648. ctype = float
  649. else:
  650. ctype = str
  651. columns[name.strip()] = { 'index' : i,
  652. 'type' : type.lower(),
  653. 'ctype' : ctype,
  654. 'length' : int(length),
  655. 'values' : [],
  656. 'ids' : []}
  657. i += 1
  658. # check for key column
  659. # v.db.connect -g/p returns always key column name lowercase
  660. if self.layers[layer]["key"] not in columns.keys():
  661. for col in columns.keys():
  662. if col.lower() == self.layers[layer]["key"]:
  663. self.layers[layer]["key"] = col.upper()
  664. break
  665. self.tables[table] = columns
  666. return True
  667. def Reset(self):
  668. """Reset"""
  669. for layer in self.layers:
  670. table = self.layers[layer]["table"] # get table desc
  671. for name in self.tables[table].keys():
  672. self.tables[table][name]['values'] = []
  673. self.tables[table][name]['ids'] = []
  674. def GetName(self):
  675. """Get vector name"""
  676. return self.map
  677. def GetKeyColumn(self, layer):
  678. """Get key column of given layer
  679. :param layer: vector layer number
  680. """
  681. return str(self.layers[layer]['key'])
  682. def GetTable(self, layer):
  683. """Get table name of given layer
  684. :param layer: vector layer number
  685. """
  686. return self.layers[layer]['table']
  687. def GetDbSettings(self, layer):
  688. """Get database settins
  689. :param layer: layer number
  690. :return: (driver, database)
  691. """
  692. return self.layers[layer]['driver'], self.layers[layer]['database']
  693. def GetTableDesc(self, table):
  694. """Get table columns
  695. :param table: table name
  696. """
  697. return self.tables[table]
  698. class LayerSelect(wx.ComboBox):
  699. def __init__(self, parent, id = wx.ID_ANY,
  700. size = globalvar.DIALOG_COMBOBOX_SIZE,
  701. vector = None, dsn = None, choices = [], all = False, default = None):
  702. """Creates combo box for selecting vector map layer names
  703. :param str vector: vector map name (native or connected via v.external)
  704. :param str dsn: OGR data source name
  705. """
  706. super(LayerSelect, self).__init__(parent, id, size = size, choices = choices)
  707. self.all = all
  708. self.SetName("LayerSelect")
  709. # default value
  710. self.default = default
  711. self.InsertLayers(vector = vector, dsn = dsn)
  712. def InsertLayers(self, vector = None, dsn = None):
  713. """Insert layers for a vector into the layer combobox
  714. :param str vector: vector map name (native or connected via v.external)
  715. :param str dsn: OGR data source name
  716. """
  717. layers = list()
  718. if vector:
  719. layers = GetVectorNumberOfLayers(vector)
  720. elif dsn:
  721. ret = RunCommand('v.in.ogr',
  722. read = True,
  723. quiet = True,
  724. flags = 'l',
  725. input = dsn)
  726. if ret:
  727. layers = ret.splitlines()
  728. if self.default:
  729. if len(layers) == 0:
  730. layers.insert(0, str(self.default))
  731. elif self.default not in layers:
  732. layers.append(self.default)
  733. if self.all:
  734. layers.insert(0, '-1')
  735. if len(layers) > 0:
  736. self.SetItems(layers)
  737. else:
  738. self.Clear()
  739. self.SetValue("")
  740. if self.default and self.default in layers:
  741. self.SetValue(self.default)
  742. class DriverSelect(wx.ComboBox):
  743. """Creates combo box for selecting database driver.
  744. """
  745. def __init__(self, parent, choices, value,
  746. id=wx.ID_ANY, pos=wx.DefaultPosition,
  747. size=globalvar.DIALOG_LAYER_SIZE, **kargs):
  748. super(DriverSelect, self).__init__(parent, id, value, pos, size,
  749. choices, style=wx.CB_READONLY)
  750. self.SetName("DriverSelect")
  751. self.SetStringSelection(value)
  752. class DatabaseSelect(wx.TextCtrl):
  753. """Creates combo box for selecting database driver.
  754. """
  755. def __init__(self, parent, value = '', id = wx.ID_ANY,
  756. size = globalvar.DIALOG_TEXTCTRL_SIZE, **kargs):
  757. super(DatabaseSelect, self).__init__(parent, id, value, size = size, **kargs)
  758. self.SetName("DatabaseSelect")
  759. class TableSelect(wx.ComboBox):
  760. """Creates combo box for selecting attribute tables from the database
  761. """
  762. def __init__(self, parent,
  763. id = wx.ID_ANY, value = '',
  764. size = globalvar.DIALOG_COMBOBOX_SIZE, choices = [], **kargs):
  765. super(TableSelect, self).__init__(parent, id, value, size = size, choices = choices,
  766. style = wx.CB_READONLY, **kargs)
  767. self.SetName("TableSelect")
  768. if not choices:
  769. self.InsertTables()
  770. def InsertTables(self, driver = None, database = None):
  771. """Insert attribute tables into combobox"""
  772. items = []
  773. if not driver or not database:
  774. connect = grass.db_connection()
  775. driver = connect['driver']
  776. database = connect['database']
  777. ret = RunCommand('db.tables',
  778. flags = 'p',
  779. read = True,
  780. driver = driver,
  781. database = database)
  782. if ret:
  783. for table in ret.splitlines():
  784. items.append(table)
  785. self.SetItems(items)
  786. self.SetValue('')
  787. class ColumnSelect(wx.combo.ComboCtrl):
  788. """Creates combo box for selecting columns in the attribute table
  789. for a vector map.
  790. :param parent: window parent
  791. :param id: window id
  792. :param value: default value
  793. :param size: window size
  794. :param str vector: vector map name
  795. :param layer: layer number
  796. :param multiple: True if it is possible to add multiple columns
  797. :param param: parameters list (see menuform.py)
  798. :param kwags: wx.ComboBox parameters
  799. """
  800. def __init__(self, parent, id = wx.ID_ANY, value = '',
  801. size = globalvar.DIALOG_COMBOBOX_SIZE,
  802. vector = None, layer = 1, multiple = False,
  803. param = None, **kwargs):
  804. self.defaultValue = value
  805. self.param = param
  806. self.columns = []
  807. wx.combo.ComboCtrl.__init__(self, parent, id, size=size, **kwargs)
  808. self.GetChildren()[0].SetName("ColumnSelect")
  809. self.GetChildren()[0].type = type
  810. self.tcp = ListCtrlComboPopup()
  811. self.SetPopupControl(self.tcp)
  812. self.tcp.SetData(multiple = multiple)
  813. if vector:
  814. self.InsertColumns(vector, layer)
  815. self.GetChildren()[0].Bind(wx.EVT_KEY_UP, self.OnKeyUp)
  816. def GetColumns(self):
  817. return self.columns
  818. def OnKeyUp(self, event):
  819. """Shows popupwindow if down arrow key is released"""
  820. if event.GetKeyCode() == wx.WXK_DOWN and not self.IsPopupShown():
  821. self.ShowPopup()
  822. else:
  823. event.Skip()
  824. def Clear(self):
  825. self.tcp.DeleteAllItems()
  826. self.SetValue('')
  827. def InsertColumns(self, vector, layer, excludeKey = False, excludeCols = None, type = None, dbInfo = None):
  828. """Insert columns for a vector attribute table into the columns combobox
  829. :param str vector: vector name
  830. :param int layer: vector layer number
  831. :param excludeKey: exclude key column from the list?
  832. :param excludeCols: list of columns to be removed from the list
  833. :param type: only columns of given type (given as list)
  834. """
  835. if not dbInfo:
  836. dbInfo = VectorDBInfo(vector)
  837. try:
  838. try:
  839. layer = int(layer)
  840. except TypeError:
  841. # assuming layer 1
  842. layer = 1
  843. table = dbInfo.GetTable(layer)
  844. columnchoices = dbInfo.GetTableDesc(table)
  845. keyColumn = dbInfo.GetKeyColumn(layer)
  846. self.columns = len(columnchoices.keys()) * ['']
  847. for key, val in columnchoices.iteritems():
  848. self.columns[val['index']] = key
  849. if excludeKey: # exclude key column
  850. self.columns.remove(keyColumn)
  851. if excludeCols: # exclude key column
  852. for key in columnchoices.iterkeys():
  853. if key in excludeCols:
  854. self.columns.remove(key)
  855. if type: # only selected column types
  856. for key, value in columnchoices.iteritems():
  857. if value['type'] not in type:
  858. try:
  859. self.columns.remove(key)
  860. except ValueError:
  861. pass
  862. except (KeyError, ValueError):
  863. self.columns[:] = []
  864. # update list
  865. self.tcp.DeleteAllItems()
  866. for col in self.columns:
  867. self.tcp.AddItem(col)
  868. self.SetValue(self.defaultValue)
  869. if self.param:
  870. value = self.param.get('value', '')
  871. if value != '' and value in self.columns:
  872. self.SetValue(value)
  873. def InsertTableColumns(self, table, driver=None, database=None):
  874. """Insert table columns
  875. :param str table: table name
  876. :param str driver: driver name
  877. :param str database: database name
  878. """
  879. self.columns[:] = []
  880. ret = RunCommand('db.columns',
  881. read = True,
  882. driver = driver,
  883. database = database,
  884. table = table)
  885. if ret:
  886. self.columns = ret.splitlines()
  887. # update list
  888. self.tcp.DeleteAllItems()
  889. self.SetValue(self.defaultValue)
  890. for col in self.columns:
  891. self.tcp.AddItem(col)
  892. if self.param:
  893. value = self.param.get('value', '')
  894. if value != '' and value in self.columns:
  895. self.SetValue(value)
  896. class DbaseSelect(wx.lib.filebrowsebutton.DirBrowseButton):
  897. """Widget for selecting GRASS Database"""
  898. def __init__(self, parent, **kwargs):
  899. super(DbaseSelect, self).__init__(parent, id = wx.ID_ANY,
  900. size = globalvar.DIALOG_GSELECT_SIZE, labelText = '',
  901. dialogTitle = _('Choose GIS Data Directory'),
  902. buttonText = _('Browse'),
  903. startDirectory = grass.gisenv()['GISDBASE'],
  904. **kwargs)
  905. class LocationSelect(wx.ComboBox):
  906. """Widget for selecting GRASS location"""
  907. def __init__(self, parent, id = wx.ID_ANY, size = globalvar.DIALOG_COMBOBOX_SIZE,
  908. gisdbase = None, **kwargs):
  909. super(LocationSelect, self).__init__(parent, id, size = size,
  910. style = wx.CB_READONLY, **kwargs)
  911. self.SetName("LocationSelect")
  912. if not gisdbase:
  913. self.gisdbase = grass.gisenv()['GISDBASE']
  914. else:
  915. self.gisdbase = gisdbase
  916. self.SetItems(GetListOfLocations(self.gisdbase))
  917. def UpdateItems(self, dbase):
  918. """Update list of locations
  919. :param str dbase: path to GIS database
  920. """
  921. self.gisdbase = dbase
  922. if dbase:
  923. self.SetItems(GetListOfLocations(self.gisdbase))
  924. else:
  925. self.SetItems([])
  926. class MapsetSelect(wx.combo.ComboCtrl):
  927. """Widget for selecting GRASS mapset"""
  928. def __init__(self, parent, id = wx.ID_ANY, size = globalvar.DIALOG_COMBOBOX_SIZE,
  929. gisdbase = None, location = None, setItems = True,
  930. searchPath = False, new = False, skipCurrent = False, multiple = False, **kwargs):
  931. style = 0
  932. ### disabled, read-only widget has no TextCtrl children (TODO: rewrite)
  933. ### if not new and not multiple:
  934. ### style = wx.CB_READONLY
  935. wx.combo.ComboCtrl.__init__(self, parent, id, size = size,
  936. style = style, **kwargs)
  937. self.searchPath = searchPath
  938. self.skipCurrent = skipCurrent
  939. self.SetName("MapsetSelect")
  940. if not gisdbase:
  941. self.gisdbase = grass.gisenv()['GISDBASE']
  942. else:
  943. self.gisdbase = gisdbase
  944. if not location:
  945. self.location = grass.gisenv()['LOCATION_NAME']
  946. else:
  947. self.location = location
  948. self.tcp = ListCtrlComboPopup()
  949. self.SetPopupControl(self.tcp)
  950. self.tcp.SetData(multiple = multiple)
  951. if setItems:
  952. self.tcp.SetItems(self._getMapsets())
  953. def UpdateItems(self, location, dbase = None):
  954. """Update list of mapsets for given location
  955. :param str dbase: path to GIS database (None to use currently
  956. selected)
  957. :param str location: name of location
  958. """
  959. if dbase:
  960. self.gisdbase = dbase
  961. self.location = location
  962. self.tcp.DeleteAllItems()
  963. if location:
  964. self.tcp.SetItems(self._getMapsets())
  965. else:
  966. self.tcp.SetItems([])
  967. def _getMapsets(self):
  968. if self.searchPath:
  969. mlist = RunCommand('g.mapsets',
  970. read = True, flags = 'p',
  971. sep = 'newline').splitlines()
  972. else:
  973. mlist = GetListOfMapsets(self.gisdbase, self.location,
  974. selectable = False)
  975. gisenv = grass.gisenv()
  976. if self.skipCurrent and \
  977. gisenv['LOCATION_NAME'] == self.location and \
  978. gisenv['MAPSET'] in mlist:
  979. mlist.remove(gisenv['MAPSET'])
  980. return mlist
  981. def GetStringSelection(self):
  982. """For backward compatibility. MapsetSelect changed to allow
  983. multiple selection, this required to change super-class from
  984. wx.ComboBox to wx.combo.ComboCtrl"""
  985. return self.GetValue()
  986. def SetStringSelection(self, text):
  987. """For backward compatibility. MapsetSelect changed to allow
  988. multiple selection, this required to change super-class from
  989. wx.ComboBox to wx.combo.ComboCtrl"""
  990. return self.SetValue(text)
  991. def SetSelection(self, sel=0):
  992. """For backward compatibility. MapsetSelect changed to allow
  993. multiple selection, this required to change super-class from
  994. wx.ComboBox to wx.combo.ComboCtrl"""
  995. self.SetValue('') # TODO: implement SetSelection()
  996. def SetItems(self, items):
  997. """For backward compatibility. MapsetSelect changed to allow
  998. multiple selection, this required to change super-class from
  999. wx.ComboBox to wx.combo.ComboCtrl"""
  1000. self.tcp.DeleteAllItems()
  1001. self.tcp.SetItems(items)
  1002. class SubGroupSelect(wx.ComboBox):
  1003. """Widget for selecting subgroups"""
  1004. def __init__(self, parent, id = wx.ID_ANY, size = globalvar.DIALOG_GSELECT_SIZE,
  1005. **kwargs):
  1006. super(SubGroupSelect, self).__init__(parent, id, size = size,
  1007. **kwargs)
  1008. self.SetName("SubGroupSelect")
  1009. def Insert(self, group):
  1010. """Insert subgroups for defined group"""
  1011. if not group:
  1012. return
  1013. gisenv = grass.gisenv()
  1014. try:
  1015. name, mapset = group.split('@', 1)
  1016. except ValueError:
  1017. name = group
  1018. mapset = gisenv['MAPSET']
  1019. mlist = RunCommand('i.group', group=group,
  1020. read=True, flags='sg').splitlines()
  1021. try:
  1022. self.SetItems(mlist)
  1023. except OSError:
  1024. self.SetItems([])
  1025. class FormatSelect(wx.Choice):
  1026. def __init__(self, parent, srcType, ogr=False,
  1027. size=globalvar.DIALOG_SPIN_SIZE,
  1028. **kwargs):
  1029. """Widget for selecting external (GDAL/OGR) format
  1030. :param parent: parent window
  1031. :param srcType: source type ('file', 'database', 'protocol')
  1032. :param ogr: True for OGR otherwise GDAL
  1033. """
  1034. super(FormatSelect, self).__init__(parent, id=wx.ID_ANY, size=size,
  1035. **kwargs)
  1036. self.SetName("FormatSelect")
  1037. if ogr:
  1038. ftype = 'ogr'
  1039. else:
  1040. ftype = 'gdal'
  1041. formats = list()
  1042. for f in GetFormats()[ftype][srcType].values():
  1043. formats += f
  1044. self.SetItems(formats)
  1045. def GetExtension(self, name):
  1046. """Get file extension by format name"""
  1047. formatToExt = dict()
  1048. formatToExt.update(rasterFormatExtension)
  1049. formatToExt.update(vectorFormatExtension)
  1050. return formatToExt.get(name, '')
  1051. # unused code since r47938
  1052. # wxGdalSelect, EVT_GDALSELECT = NewEvent()
  1053. class GdalSelect(wx.Panel):
  1054. def __init__(self, parent, panel, ogr=False, link=False, dest=False,
  1055. exclude=None):
  1056. """Widget for selecting GDAL/OGR datasource, format
  1057. .. todo::
  1058. Split into GdalSelect and OgrSelect and optionally to
  1059. GdalSelectOutput, OgrSelectOutput
  1060. :param parent: parent window
  1061. :param bool ogr: use OGR selector instead of GDAL
  1062. :param bool dest: True for output (destination)
  1063. :param default: default type (ignored when dest == True)
  1064. :param exclude: list of types to be excluded
  1065. """
  1066. self.parent = parent
  1067. self.ogr = ogr
  1068. self.link = link
  1069. self.dest = dest
  1070. self._sourceType = None
  1071. wx.Panel.__init__(self, parent=panel)
  1072. self.reloadDataRequired = Signal('GdalSelect.reloadDataRequired')
  1073. if self.ogr:
  1074. settingsFile = os.path.join(GetSettingsPath(), 'wxOGR')
  1075. else:
  1076. settingsFile = os.path.join(GetSettingsPath(), 'wxGDAL')
  1077. self.settsManager = ManageSettingsWidget(parent=self,
  1078. settingsFile=settingsFile)
  1079. self.settsManager.settingsChanged.connect(self.OnSettingsChanged)
  1080. self.settsManager.settingsSaving.connect(self.OnSettingsSaving)
  1081. self.inputBox = wx.StaticBox(parent=self)
  1082. if dest:
  1083. self.inputBox.SetLabel(" %s " % _("Output settings"))
  1084. else:
  1085. self.inputBox.SetLabel(" %s " % _("Source settings"))
  1086. # source type
  1087. sources = list()
  1088. self.sourceMap = { 'file' : -1,
  1089. 'dir' : -1,
  1090. 'db' : -1,
  1091. 'pro' : -1,
  1092. 'native' : -1 }
  1093. idx = 0
  1094. if dest:
  1095. sources.append(_("Native"))
  1096. self.sourceMap['native'] = idx
  1097. idx += 1
  1098. if exclude is None:
  1099. exclude = []
  1100. if 'file' not in exclude:
  1101. sources.append(_("File"))
  1102. self.sourceMap['file'] = idx
  1103. idx += 1
  1104. if 'directory' not in exclude:
  1105. sources.append(_("Directory"))
  1106. self.sourceMap['dir'] = idx
  1107. idx += 1
  1108. if 'database' not in exclude:
  1109. sources.append(_("Database"))
  1110. self.sourceMap['db'] = idx
  1111. idx += 1
  1112. if 'protocol' not in exclude:
  1113. sources.append(_("Protocol"))
  1114. self.sourceMap['pro'] = idx
  1115. idx += 1
  1116. self.sourceMapByIdx = {}
  1117. for name, idx in self.sourceMap.items():
  1118. self.sourceMapByIdx[ idx ] = name
  1119. self.source = wx.RadioBox(parent=self, id=wx.ID_ANY,
  1120. style=wx.RA_SPECIFY_COLS,
  1121. choices=sources)
  1122. if dest:
  1123. self.source.SetLabel(" %s " % _('Output type'))
  1124. else:
  1125. self.source.SetLabel(" %s " % _('Source type'))
  1126. self.source.SetSelection(0)
  1127. self.source.Bind(wx.EVT_RADIOBOX,
  1128. lambda evt: self.SetSourceType(self.sourceMapByIdx[evt.GetInt()]))
  1129. self.nativeWidgets = {}
  1130. self.fileWidgets = {}
  1131. self.dirWidgets = {}
  1132. self.dbWidgets = {}
  1133. self.protocolWidgets = {}
  1134. self.pgWidgets = {}
  1135. if ogr:
  1136. fType = 'ogr'
  1137. else:
  1138. fType = 'gdal'
  1139. # file
  1140. fileMask = '%(all)s (*)|*|' % {'all': _('All files')}
  1141. if not ogr:
  1142. extList = rasterFormatExtension
  1143. fileMask += ('%(name)s (*.%(low1)s;*.%(low2)s;*.%(up1)s;*.%(up2)s)|'
  1144. '*.%(low1)s;*.%(low2)s;*.%(up1)s;*.%(up2)s|' %
  1145. {'name': 'GeoTIFF', 'low1': 'tif', 'low2': 'tiff', 'up1': 'TIF', 'up2': 'TIFF'})
  1146. else:
  1147. extList = vectorFormatExtension
  1148. fileMask += '%(name)s (*.%(low)s;*.%(up)s)|*.%(low)s;*.%(up)s|' % {'name': 'ESRI Shapefile', 'low': 'shp', 'up': 'SHP'}
  1149. for name, ext in sorted(extList.items()):
  1150. if name in ('ESRI Shapefile', 'GeoTIFF'):
  1151. continue
  1152. fileMask += '%(name)s (*.%(low)s;*.%(up)s)|*.%(low)s;*.%(up)s|' % {'name': name,
  1153. 'low': ext.lower(),
  1154. 'up': ext.upper()}
  1155. fileMask += '%s (*.zip;*.ZIP)|*.zip;*.ZIP|' % _('ZIP files')
  1156. fileMask += '%s (*.gz;*.GZ)|*.gz;*.GZ|' % _('GZIP files')
  1157. fileMask += '%s (*.tar;*.TAR)|*.tar;*.TAR|' % _('TAR files')
  1158. # don't include last '|' - windows and mac throw error
  1159. fileMask += '%s (*.tar.gz;*.TAR.GZ;*.tgz;*.TGZ)|*.tar.gz;*.TAR.GZ;*.tgz;*.TGZ' % _('TARGZ files')
  1160. # only contains formats with extensions hardcoded
  1161. self.filePanel = wx.Panel(parent=self)
  1162. browse = filebrowse.FileBrowseButton(parent=self.filePanel, id=wx.ID_ANY,
  1163. size=globalvar.DIALOG_GSELECT_SIZE,
  1164. labelText = _('File:'),
  1165. dialogTitle=_('Choose file to import'),
  1166. buttonText=_('Browse'),
  1167. startDirectory=os.getcwd(),
  1168. changeCallback=self.OnUpdate,
  1169. fileMask=fileMask)
  1170. self.fileWidgets['browse'] = browse
  1171. self.fileWidgets['options'] = wx.TextCtrl(parent=self.filePanel)
  1172. # directory
  1173. self.dirPanel = wx.Panel(parent=self)
  1174. browse = filebrowse.DirBrowseButton(parent=self.dirPanel, id=wx.ID_ANY,
  1175. size=globalvar.DIALOG_GSELECT_SIZE,
  1176. labelText=_('Directory:'),
  1177. dialogTitle=_('Choose input directory'),
  1178. buttonText=_('Browse'),
  1179. startDirectory=os.getcwd(),
  1180. changeCallback=self.OnUpdate)
  1181. self.dirWidgets['browse'] = browse
  1182. formatSelect = wx.Choice(parent=self.dirPanel, size=(300, -1))
  1183. self.dirWidgets['format'] = formatSelect
  1184. fileFormats = GetFormats(writableOnly=dest)[fType]['file']
  1185. formatSelect.SetItems(sorted(list(fileFormats)))
  1186. formatSelect.Bind(wx.EVT_CHOICE, lambda evt: self.SetExtension(self.dirWidgets['format'].GetStringSelection()))
  1187. formatSelect.Bind(wx.EVT_CHOICE, self.OnUpdate)
  1188. self.dirWidgets['extensionLabel'] = wx.StaticText(parent=self.dirPanel,
  1189. label = _("Extension:"))
  1190. self.dirWidgets['extension'] = wx.TextCtrl(parent=self.dirPanel)
  1191. self.dirWidgets['extension'].Bind(wx.EVT_TEXT, self.ExtensionChanged)
  1192. self.dirWidgets['options'] = wx.TextCtrl(parent=self.dirPanel)
  1193. if self.ogr:
  1194. shapefile = 'ESRI Shapefile'
  1195. if shapefile in fileFormats:
  1196. formatSelect.SetStringSelection(shapefile)
  1197. self.SetExtension(shapefile)
  1198. else:
  1199. tiff = 'GeoTIFF'
  1200. if tiff in fileFormats:
  1201. formatSelect.SetStringSelection(tiff)
  1202. self.SetExtension(tiff)
  1203. # database
  1204. self.dbPanel = wx.Panel(parent=self)
  1205. self.dbFormats = GetFormats(writableOnly=dest)[fType]['database']
  1206. dbChoice = wx.Choice(parent=self.dbPanel, choices=self.dbFormats)
  1207. dbChoice.Bind(wx.EVT_CHOICE, lambda evt: self.SetDatabase(db=dbChoice.GetStringSelection()))
  1208. self.dbWidgets['format'] = dbChoice
  1209. browse = filebrowse.FileBrowseButton(parent=self.dbPanel, id=wx.ID_ANY,
  1210. size=globalvar.DIALOG_GSELECT_SIZE,
  1211. labelText=_("Name:"),
  1212. dialogTitle=_('Choose file'),
  1213. buttonText=_('Browse'),
  1214. startDirectory=os.getcwd(),
  1215. changeCallback=self.OnUpdate)
  1216. self.dbWidgets['browse'] = browse
  1217. self.dbWidgets['choice'] = wx.Choice(parent=self.dbPanel)
  1218. self.dbWidgets['choice'].Bind(wx.EVT_CHOICE, self.OnUpdate)
  1219. self.dbWidgets['text'] = wx.TextCtrl(parent=self.dbPanel)
  1220. self.dbWidgets['text'].Bind(wx.EVT_TEXT, self.OnUpdate)
  1221. self.dbWidgets['textLabel1'] = wx.StaticText(parent=self.dbPanel, label=_("Name:"))
  1222. self.dbWidgets['textLabel2'] = wx.StaticText(parent=self.dbPanel, label=_("Name:"))
  1223. self.dbWidgets['featType'] = wx.RadioBox(parent=self.dbPanel, id=wx.ID_ANY,
  1224. label = " %s " % _("Feature type:"),
  1225. choices = [_("simple features"), _("topological")],
  1226. majorDimension=2,
  1227. style = wx.RA_SPECIFY_COLS)
  1228. if dest:
  1229. self.dbWidgets['featType'].Disable()
  1230. else:
  1231. self.dbWidgets['featType'].Hide()
  1232. browse = filebrowse.DirBrowseButton(parent=self.dbPanel, id=wx.ID_ANY,
  1233. size=globalvar.DIALOG_GSELECT_SIZE,
  1234. labelText=_('Directory:'),
  1235. dialogTitle=_('Choose input directory'),
  1236. buttonText=_('Browse'),
  1237. startDirectory=os.getcwd(),
  1238. changeCallback=self.OnUpdate)
  1239. self.dbWidgets['dirbrowse'] = browse
  1240. self.dbWidgets['options'] = wx.TextCtrl(parent=self.dbPanel)
  1241. # protocol
  1242. self.protocolPanel = wx.Panel(parent=self)
  1243. protocolFormats = GetFormats(writableOnly=self.dest)[fType]['protocol']
  1244. protocolChoice = wx.Choice(parent=self.protocolPanel, choices=protocolFormats)
  1245. self.protocolWidgets['format'] = protocolChoice
  1246. self.protocolWidgets['text'] = wx.TextCtrl(parent=self.protocolPanel)
  1247. self.protocolWidgets['text'].Bind(wx.EVT_TEXT, self.OnUpdate)
  1248. self.protocolWidgets['options'] = wx.TextCtrl(parent=self.protocolPanel)
  1249. # native
  1250. self.nativePanel = wx.Panel(parent=self)
  1251. self._layout()
  1252. sourceType = 'file'
  1253. self.SetSourceType(sourceType) # needed always to fit dialog size
  1254. if self.dest:
  1255. current = RunCommand('v.external.out',
  1256. parent=self,
  1257. read=True, parse=grass.parse_key_val,
  1258. flags='g')
  1259. if current['format'] == 'native':
  1260. sourceType = 'native'
  1261. elif current['format'] in GetFormats()['ogr']['database']:
  1262. sourceType = 'db'
  1263. else:
  1264. sourceType = 'dir'
  1265. if self.dest:
  1266. wx.CallAfter(self._postInit, sourceType, current)
  1267. def _postInit(self, sourceType, data):
  1268. """Fill in default values."""
  1269. format = data.get('format', '')
  1270. pg = 'conninfo' in data.keys()
  1271. if pg:
  1272. dsn=''
  1273. for item in data.get('conninfo').split(' '):
  1274. k, v = item.split('=')
  1275. if k == 'dbname':
  1276. dsn=v
  1277. break
  1278. optList = list()
  1279. for k, v in data.iteritems():
  1280. if k in ('format', 'conninfo', 'topology'):
  1281. continue
  1282. optList.append('%s=%s' % (k, v))
  1283. options = ','.join(optList)
  1284. else:
  1285. dsn = data.get('dsn')
  1286. options = data.get('options', '')
  1287. self.SetSourceType(sourceType)
  1288. self.source.SetSelection(self.sourceMap[sourceType])
  1289. dsn = os.path.expandvars(dsn) # v.external.out uses $HOME
  1290. # fill in default values
  1291. if sourceType == 'dir':
  1292. self.dirWidgets['format'].SetStringSelection(format)
  1293. self.dirWidgets['browse'].SetValue(dsn)
  1294. self.dirWidgets['options'].SetValue(options)
  1295. elif sourceType == 'db':
  1296. self.dbWidgets['format'].SetStringSelection(format)
  1297. self.dbWidgets['options'].SetValue(options)
  1298. name = self._getCurrentDbWidgetName()
  1299. if name == 'choice':
  1300. if dsn in self.dbWidgets[name].GetItems():
  1301. self.dbWidgets[name].SetStringSelection(dsn)
  1302. if 'topology' in data.keys():
  1303. self.dbWidgets['featType'].SetSelection(1)
  1304. else:
  1305. self.dbWidgets[name].SetValue(dsn)
  1306. def _layout(self):
  1307. """Layout"""
  1308. mainSizer = wx.BoxSizer(wx.VERTICAL)
  1309. self.changingSizer = wx.StaticBoxSizer(self.inputBox, wx.VERTICAL)
  1310. # file
  1311. paddingSizer = wx.BoxSizer(wx.VERTICAL)
  1312. sizer = wx.GridBagSizer(vgap=5, hgap=10)
  1313. paddingSizer.Add(item=self.fileWidgets['browse'],
  1314. flag=wx.BOTTOM | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND,
  1315. border=30)
  1316. sizer.Add(item=paddingSizer, flag=wx.EXPAND, pos=(0, 0), span=(1, 2))
  1317. sizer.AddGrowableCol(0)
  1318. if self.dest:
  1319. sizer.Add(item=wx.StaticText(parent=self.filePanel,
  1320. label = _("Creation options:")),
  1321. flag = wx.ALIGN_CENTER_VERTICAL,
  1322. pos = (1, 0))
  1323. sizer.Add(item=self.fileWidgets['options'],
  1324. flag=wx.ALIGN_CENTER_VERTICAL|wx.EXPAND,
  1325. pos=(1, 1))
  1326. else:
  1327. self.fileWidgets['options'].Hide()
  1328. self.filePanel.SetSizer(sizer)
  1329. # directory
  1330. sizer = wx.GridBagSizer(vgap=3, hgap=10)
  1331. sizer.Add(item=wx.StaticText(parent=self.dirPanel,
  1332. label = _("Format:")),
  1333. flag=wx.ALIGN_CENTER_VERTICAL,
  1334. pos=(0, 0))
  1335. sizer.Add(item=self.dirWidgets['format'],
  1336. flag=wx.ALIGN_CENTER_VERTICAL|wx.EXPAND,
  1337. pos=(0, 1))
  1338. sizer.Add(item=self.dirWidgets['extensionLabel'],
  1339. flag=wx.ALIGN_CENTER_VERTICAL,
  1340. pos=(0, 2))
  1341. sizer.Add(item=self.dirWidgets['extension'],
  1342. flag=wx.ALIGN_CENTER_VERTICAL,
  1343. pos=(0, 3))
  1344. sizer.Add(item=self.dirWidgets['browse'],
  1345. flag=wx.ALIGN_CENTER_VERTICAL|wx.EXPAND,
  1346. pos=(1, 0), span=(1, 4))
  1347. if self.dest:
  1348. sizer.Add(item=wx.StaticText(parent=self.dirPanel,
  1349. label = _("Creation options:")),
  1350. flag = wx.ALIGN_CENTER_VERTICAL,
  1351. pos = (2, 0))
  1352. sizer.Add(item=self.dirWidgets['options'],
  1353. flag=wx.ALIGN_CENTER_VERTICAL|wx.EXPAND,
  1354. pos=(2, 1))
  1355. helpBtn = wx.Button(parent=self.dirPanel, id=wx.ID_HELP)
  1356. helpBtn.Bind(wx.EVT_BUTTON, self.OnHelp)
  1357. sizer.Add(item=helpBtn,
  1358. flag=wx.ALIGN_CENTER_VERTICAL|wx.EXPAND,
  1359. pos=(2, 2))
  1360. self.dirWidgets['extensionLabel'].Hide()
  1361. self.dirWidgets['extension'].Hide()
  1362. else:
  1363. self.dirWidgets['options'].Hide()
  1364. sizer.AddGrowableCol(1)
  1365. self.dirPanel.SetSizer(sizer)
  1366. # database
  1367. sizer = wx.GridBagSizer(vgap=1, hgap=5)
  1368. sizer.Add(item=wx.StaticText(parent=self.dbPanel,
  1369. label = _("Format:")),
  1370. flag=wx.ALIGN_CENTER_VERTICAL,
  1371. pos=(0, 0))
  1372. sizer.Add(item=self.dbWidgets['format'],
  1373. flag=wx.ALIGN_CENTER_VERTICAL,
  1374. pos=(0, 1))
  1375. sizer.Add(item=self.dbWidgets['textLabel1'],
  1376. flag=wx.ALIGN_CENTER_VERTICAL,
  1377. pos=(1, 0))
  1378. sizer.Add(item=self.dbWidgets['text'],
  1379. flag=wx.ALIGN_CENTER_VERTICAL|wx.EXPAND,
  1380. pos=(1, 1), span=(1, 2))
  1381. sizer.Add(item=self.dbWidgets['browse'],
  1382. flag=wx.ALIGN_CENTER_VERTICAL|wx.EXPAND,
  1383. pos=(2, 0), span=(1, 3))
  1384. sizer.Add(item=self.dbWidgets['dirbrowse'],
  1385. flag=wx.ALIGN_CENTER_VERTICAL|wx.EXPAND,
  1386. pos=(3, 0), span=(1, 2))
  1387. sizer.Add(item=self.dbWidgets['textLabel2'],
  1388. flag=wx.ALIGN_CENTER_VERTICAL,
  1389. pos=(4, 0))
  1390. sizer.Add(item=self.dbWidgets['choice'],
  1391. flag=wx.ALIGN_CENTER_VERTICAL|wx.EXPAND,
  1392. pos=(4, 1), span=(1, 2))
  1393. if self.dest:
  1394. sizer.Add(item=self.dbWidgets['featType'],
  1395. pos=(0, 2), flag=wx.EXPAND)
  1396. sizer.Add(item=wx.StaticText(parent=self.dbPanel,
  1397. label = _("Creation options:")),
  1398. flag = wx.ALIGN_CENTER_VERTICAL,
  1399. pos = (5, 0))
  1400. sizer.Add(item=self.dbWidgets['options'],
  1401. flag=wx.ALIGN_CENTER_VERTICAL|wx.EXPAND,
  1402. pos=(5, 1), span=(1, 2))
  1403. # help button
  1404. helpBtn = wx.Button(parent=self.dbPanel, id=wx.ID_HELP)
  1405. helpBtn.Bind(wx.EVT_BUTTON, self.OnHelp)
  1406. sizer.Add(item=helpBtn,
  1407. pos=(5, 3))
  1408. else:
  1409. self.dbWidgets['options'].Hide()
  1410. self.dbPanel.SetSizer(sizer)
  1411. sizer.SetEmptyCellSize((0, 0))
  1412. sizer.AddGrowableCol(1)
  1413. # protocol
  1414. sizer = wx.GridBagSizer(vgap=3, hgap=3)
  1415. sizer.Add(item=wx.StaticText(parent=self.protocolPanel,
  1416. label = _("Format:")),
  1417. flag=wx.ALIGN_CENTER_VERTICAL,
  1418. pos=(0, 0))
  1419. sizer.Add(item=self.protocolWidgets['format'],
  1420. flag=wx.ALIGN_CENTER_VERTICAL,
  1421. pos=(0, 1))
  1422. sizer.Add(item=wx.StaticText(parent=self.protocolPanel,
  1423. label = _("Protocol:")),
  1424. flag=wx.ALIGN_CENTER_VERTICAL,
  1425. pos=(1, 0))
  1426. sizer.Add(item=self.protocolWidgets['text'],
  1427. flag=wx.ALIGN_CENTER_VERTICAL|wx.EXPAND,
  1428. pos=(1, 1))
  1429. if self.dest:
  1430. sizer.Add(item=wx.StaticText(parent=self.protocolPanel,
  1431. label = _("Creation options:")),
  1432. flag = wx.ALIGN_CENTER_VERTICAL,
  1433. pos = (2, 0))
  1434. sizer.Add(item=self.protocolWidgets['options'],
  1435. flag=wx.ALIGN_CENTER_VERTICAL|wx.EXPAND,
  1436. pos=(2, 1))
  1437. else:
  1438. self.protocolWidgets['options'].Hide()
  1439. sizer.AddGrowableCol(1)
  1440. self.protocolPanel.SetSizer(sizer)
  1441. # native
  1442. sizer = wx.BoxSizer(wx.VERTICAL)
  1443. sizer.Add(item=wx.StaticText(parent=self.nativePanel,
  1444. label = _("No settings available")),
  1445. flag=wx.ALIGN_CENTER_VERTICAL|wx.ALL|wx.EXPAND, border=5)
  1446. self.nativePanel.SetSizer(sizer)
  1447. for panel in (self.nativePanel, self.filePanel,
  1448. self.dirPanel, self.dbPanel,
  1449. self.protocolPanel):
  1450. self.changingSizer.Add(item=panel, proportion=1,
  1451. flag=wx.EXPAND)
  1452. mainSizer.Add(item=self.settsManager, proportion=0,
  1453. flag=wx.ALL | wx.EXPAND, border=5)
  1454. mainSizer.Add(item=self.source, proportion=0,
  1455. flag=wx.LEFT | wx.RIGHT | wx.EXPAND, border=5)
  1456. mainSizer.Add(item=self.changingSizer, proportion=1,
  1457. flag=wx.ALL | wx.EXPAND, border=5)
  1458. self.mainSizer = mainSizer
  1459. self.SetSizer(mainSizer)
  1460. mainSizer.Fit(self)
  1461. def _getExtension(self, name):
  1462. """Get file extension by format name"""
  1463. formatToExt = dict()
  1464. formatToExt.update(rasterFormatExtension)
  1465. formatToExt.update(vectorFormatExtension)
  1466. return formatToExt.get(name, '')
  1467. def SetSourceType(self, sourceType):
  1468. """Set source type (db, file, dir, ...).
  1469. Does not switch radioboxes."""
  1470. self._sourceType = sourceType
  1471. self.changingSizer.Show(item=self.filePanel, show=(sourceType == 'file'))
  1472. self.changingSizer.Show(item=self.nativePanel, show=(sourceType == 'native'))
  1473. self.changingSizer.Show(item=self.dirPanel, show=(sourceType == 'dir'))
  1474. self.changingSizer.Show(item=self.protocolPanel, show=(sourceType == 'pro'))
  1475. self.changingSizer.Show(item=self.dbPanel, show=(sourceType is 'db'))
  1476. self.changingSizer.Layout()
  1477. if sourceType == 'db':
  1478. self.dbWidgets['format'].SetItems(self.dbFormats)
  1479. if self.dbFormats:
  1480. if 'PostgreSQL' in self.dbFormats:
  1481. self.dbWidgets['format'].SetStringSelection('PostgreSQL')
  1482. else:
  1483. self.dbWidgets['format'].SetSelection(0)
  1484. self.dbWidgets['format'].Enable()
  1485. if sourceType == 'db':
  1486. db = self.dbWidgets['format'].GetStringSelection()
  1487. self.SetDatabase(db)
  1488. if not self.dest:
  1489. self.reloadDataRequired.emit(data=None)
  1490. self._reloadLayers()
  1491. def OnSettingsChanged(self, data):
  1492. """User changed setting"""
  1493. # data list: [type, dsn, format, options]
  1494. if len(data) == 3:
  1495. data.append('')
  1496. elif len < 3:
  1497. return
  1498. self.source.SetSelection(self.sourceMap[data[0]])
  1499. self.SetSourceType(data[0])
  1500. if data[0] == 'file':
  1501. self.fileWidgets['browse'].SetValue(data[1])
  1502. self.fileWidgets['options'].SetValue(data[3])
  1503. elif data[0] == 'dir':
  1504. self.dirWidgets['browse'].SetValue(data[1])
  1505. self.dirWidgets['format'].SetStringSelection(data[2])
  1506. self.dirWidgets['options'].SetValue(data[3])
  1507. self.SetExtension(data[2])
  1508. elif data[0] == 'pro':
  1509. self.protocolWidgets['text'].SetValue(data[2])
  1510. self.protocolWidgets['options'].SetValue(data[3])
  1511. elif data[0] == 'db':
  1512. name = self._getCurrentDbWidgetName()
  1513. if name =='choice':
  1514. if len(data[1].split(':', 1)) > 1:
  1515. for item in data[1].split(':', 1)[1].split(','):
  1516. key, value = item.split('=', 1)
  1517. if key == 'dbname':
  1518. self.dbWidgets[name].SetStringSelection(value)
  1519. break
  1520. else:
  1521. self.dbWidgets[name].SetStringSelection(data[1])
  1522. else:
  1523. self.dbWidgets[name].SetValue(data[1])
  1524. self.dbWidgets['options'].SetValue(data[3])
  1525. if not self.dest:
  1526. self.reloadDataRequired.emit(data=None)
  1527. self._reloadLayers()
  1528. def OnSettingsSaving(self, name):
  1529. """Saving data"""
  1530. if not self.GetDsn():
  1531. GMessage(parent = self,
  1532. message = _("No data source defined, settings are not saved."))
  1533. return
  1534. self.settsManager.SetDataToSave((self._sourceType, self.GetDsn(),
  1535. self.GetFormat(), self.GetOptions()))
  1536. self.settsManager.SaveSettings(name)
  1537. def _getExtPatternGlob(self, ext):
  1538. """Get pattern for case-insensitive globing"""
  1539. pattern = '*.'
  1540. for c in ext:
  1541. pattern += '[%s%s]' % (c.lower(), c.upper())
  1542. return pattern
  1543. def _getCurrentDbWidgetName(self):
  1544. """Returns active dns database widget name."""
  1545. for widget in ('browse', 'dirbrowse', 'text', 'choice'):
  1546. if self.dbWidgets[widget].IsShown():
  1547. return widget
  1548. def GetDsn(self):
  1549. """Get datasource name
  1550. """
  1551. if self._sourceType == 'db':
  1552. if self.dbWidgets['format'].GetStringSelection() in ('PostgreSQL',
  1553. 'PostGIS Raster driver'):
  1554. dsn = 'PG:dbname=%s' % self.dbWidgets['choice'].GetStringSelection()
  1555. else:
  1556. name = self._getCurrentDbWidgetName()
  1557. if name == 'choice':
  1558. dsn = self.dbWidgets[name].GetStringSelection()
  1559. else:
  1560. dsn = self.dbWidgets[name].GetValue()
  1561. else:
  1562. if self._sourceType == 'file':
  1563. dsn = self.fileWidgets['browse'].GetValue()
  1564. elif self._sourceType == 'dir':
  1565. dsn = self.dirWidgets['browse'].GetValue()
  1566. elif self._sourceType == 'pro':
  1567. dsn = self.protocolWidgets['text'].GetValue()
  1568. else:
  1569. dsn = ''
  1570. # check compressed files
  1571. try:
  1572. ext = os.path.splitext(dsn)[1].lower()
  1573. except KeyError:
  1574. ext = None
  1575. if ext == '.zip':
  1576. dsn = '/vsizip/' + dsn
  1577. elif ext == '.gzip':
  1578. dsn = '/vsigzip/' + dsn
  1579. elif ext in ('.tar', '.tar.gz', '.tgz'):
  1580. dsn = '/vsitar/' + dsn
  1581. return dsn
  1582. def SetDatabase(self, db):
  1583. """Update database panel."""
  1584. sizer = self.dbPanel.GetSizer()
  1585. showBrowse = db in ('SQLite', 'Rasterlite')
  1586. showDirbrowse = db in ('FileGDB')
  1587. showChoice = db in ('PostgreSQL','PostGIS WKT Raster driver',
  1588. 'PostGIS Raster driver')
  1589. enableFeatType = self.dest and self.ogr and db in ('PostgreSQL')
  1590. showText = not(showBrowse or showChoice or showDirbrowse)
  1591. sizer.Show(self.dbWidgets['browse'], show=showBrowse)
  1592. sizer.Show(self.dbWidgets['dirbrowse'], show=showDirbrowse)
  1593. sizer.Show(self.dbWidgets['choice'], show=showChoice)
  1594. sizer.Show(self.dbWidgets['textLabel2'], show=showChoice)
  1595. sizer.Show(self.dbWidgets['text'], show=showText)
  1596. sizer.Show(self.dbWidgets['textLabel1'], show=showText)
  1597. self.dbWidgets['featType'].Enable(enableFeatType)
  1598. if showChoice:
  1599. # try to get list of PG databases
  1600. dbNames = RunCommand('db.databases', parent=self, quiet=True, read=True,
  1601. driver='pg').splitlines()
  1602. if dbNames is not None:
  1603. self.dbWidgets['choice'].SetItems(sorted(dbNames))
  1604. self.dbWidgets['choice'].SetSelection(0)
  1605. elif grass.find_program('psql', '--help'):
  1606. if not self.dbWidgets['choice'].GetItems():
  1607. p = grass.Popen(['psql', '-ltA'], stdout = grass.PIPE)
  1608. ret = p.communicate()[0]
  1609. if ret:
  1610. dbNames = list()
  1611. for line in ret.splitlines():
  1612. sline = line.split('|')
  1613. if len(sline) < 2:
  1614. continue
  1615. dbname = sline[0]
  1616. if dbname:
  1617. dbNames.append(dbname)
  1618. self.dbWidgets['choice'].SetItems(db)
  1619. self.dbWidgets['choice'].SetSelection(0)
  1620. else:
  1621. sizer.Show(self.dbWidgets['text'])
  1622. sizer.Show(self.dbWidgets['choice'], False)
  1623. sizer.Layout()
  1624. def OnUpdate(self, event):
  1625. """Update required - load layers."""
  1626. if not self.dest:
  1627. self._reloadLayers()
  1628. event.Skip()
  1629. def _reloadLayers(self):
  1630. """Reload list of layers"""
  1631. dsn = self.GetDsn()
  1632. if not dsn:
  1633. return
  1634. data = list()
  1635. layerId = 1
  1636. if self.ogr:
  1637. ret = RunCommand('v.external',
  1638. quiet = True,
  1639. read = True,
  1640. flags = 't',
  1641. input = dsn)
  1642. if not ret:
  1643. self.reloadDataRequired.emit(data=None)
  1644. return
  1645. layerId = 1
  1646. for line in ret.splitlines():
  1647. layerName, featureType, projection, geometryColumn = map(lambda x: x.strip(), line.split(','))
  1648. if projection == '0':
  1649. projectionMatch = _("No")
  1650. else:
  1651. projectionMatch = _("Yes")
  1652. grassName = GetValidLayerName(layerName)
  1653. if geometryColumn:
  1654. featureType = geometryColumn + '/' + featureType
  1655. data.append((layerId, layerName, featureType, projectionMatch, grassName))
  1656. layerId += 1
  1657. else:
  1658. if self._sourceType == 'file':
  1659. baseName = os.path.basename(dsn)
  1660. grassName = GetValidLayerName(baseName.split('.', -1)[0])
  1661. data.append((layerId, baseName, grassName))
  1662. elif self._sourceType == 'dir':
  1663. ext = self.dirWidgets['extension'].GetValue()
  1664. for filename in glob.glob(os.path.join(dsn, "%s") % self._getExtPatternGlob(ext)):
  1665. baseName = os.path.basename(filename)
  1666. grassName = GetValidLayerName(baseName.split('.', -1)[0])
  1667. data.append((layerId, baseName, grassName))
  1668. layerId += 1
  1669. # unused code since r47938
  1670. # if self.ogr:
  1671. # dsn += '@OGR'
  1672. #
  1673. # evt = wxGdalSelect(dsn = dsn)
  1674. # evt.SetId(self.input[self.dsnType][1].GetId())
  1675. # wx.PostEvent(self.parent, evt)
  1676. if self.parent.GetName() == 'MultiImportDialog':
  1677. self.reloadDataRequired.emit(data=data)
  1678. def ExtensionChanged(self, event):
  1679. if not self.dest:
  1680. # reload layers
  1681. self._reloadLayers()
  1682. def SetExtension(self, name):
  1683. """Extension changed"""
  1684. ext = self._getExtension(name)
  1685. self.dirWidgets['extension'].SetValue(ext)
  1686. def GetType(self):
  1687. """Get source type"""
  1688. return self._sourceType
  1689. def GetFormat(self):
  1690. """Get format as string"""
  1691. if self._sourceType == 'dir':
  1692. format = self.dirWidgets['format'].GetStringSelection()
  1693. elif self._sourceType == 'pro':
  1694. format = self.protocolWidgets['format'].GetStringSelection()
  1695. elif self._sourceType == 'db':
  1696. format = self.dbWidgets['format'].GetStringSelection()
  1697. else:
  1698. format = ''
  1699. return format.replace(' ', '_')
  1700. def GetFormatExt(self):
  1701. """Get format extension"""
  1702. return self._getExtension(self.GetFormat())
  1703. def GetOptions(self):
  1704. """Get creation options"""
  1705. if self._sourceType == 'file':
  1706. options = self.fileWidgets['options'].GetValue()
  1707. elif self._sourceType == 'dir':
  1708. options = self.dirWidgets['options'].GetValue()
  1709. elif self._sourceType == 'pro':
  1710. options = self.protocolWidgets['options'].GetValue()
  1711. elif self._sourceType == 'db':
  1712. if self.dbWidgets['featType'].GetSelection() == 1:
  1713. options = 'topology=yes '
  1714. else:
  1715. options = ''
  1716. options += self.dbWidgets['options'].GetValue()
  1717. return options.strip()
  1718. def OnHelp(self, event):
  1719. """Show related manual page"""
  1720. cmd = ''
  1721. if self.dest:
  1722. if self.ogr:
  1723. cmd = 'v.external.out'
  1724. else:
  1725. cmd = 'r.external.out'
  1726. else:
  1727. if self.link:
  1728. if self.ogr:
  1729. cmd = 'v.external'
  1730. else:
  1731. cmd = 'r.external'
  1732. else:
  1733. if self.ogr:
  1734. cmd = 'v.in.ogr'
  1735. else:
  1736. cmd = 'r.in.gdal'
  1737. RunCommand('g.manual', entry = cmd)
  1738. class ProjSelect(wx.ComboBox):
  1739. """Widget for selecting input raster/vector map used by
  1740. r.proj/v.proj modules."""
  1741. def __init__(self, parent, isRaster, id = wx.ID_ANY, size = globalvar.DIALOG_COMBOBOX_SIZE,
  1742. **kwargs):
  1743. super(ProjSelect, self).__init__(parent, id, size = size,
  1744. style = wx.CB_READONLY, **kwargs)
  1745. self.SetName("ProjSelect")
  1746. self.isRaster = isRaster
  1747. def UpdateItems(self, dbase, location, mapset):
  1748. """Update list of maps
  1749. """
  1750. if not dbase:
  1751. dbase = grass.gisenv()['GISDBASE']
  1752. if not mapset:
  1753. mapset = grass.gisenv()['MAPSET']
  1754. if self.isRaster:
  1755. ret = RunCommand('r.proj',
  1756. quiet = True,
  1757. read = True,
  1758. flags = 'l',
  1759. dbase = dbase,
  1760. location = location,
  1761. mapset = mapset)
  1762. else:
  1763. ret = RunCommand('v.proj',
  1764. quiet = True,
  1765. read = True,
  1766. flags = 'l',
  1767. dbase = dbase,
  1768. location = location,
  1769. mapset = mapset)
  1770. listMaps = list()
  1771. if ret:
  1772. for line in ret.splitlines():
  1773. listMaps.append(line.strip())
  1774. ListSortLower(listMaps)
  1775. self.SetItems(listMaps)
  1776. self.SetValue('')
  1777. class ElementSelect(wx.Choice):
  1778. def __init__(self, parent, id = wx.ID_ANY, elements = None,
  1779. size = globalvar.DIALOG_COMBOBOX_SIZE,
  1780. **kwargs):
  1781. """Widget for selecting GIS element
  1782. :param parent: parent window
  1783. :param elements: filter elements
  1784. """
  1785. super(ElementSelect, self).__init__(parent, id, size = size,
  1786. **kwargs)
  1787. self.SetName("ElementSelect")
  1788. task = gtask.parse_interface('g.list')
  1789. p = task.get_param(value = 'type')
  1790. self.values = p.get('values', [])
  1791. self.valuesDesc = p.get('values_desc', [])
  1792. if elements:
  1793. values = []
  1794. valuesDesc = []
  1795. for idx in range(0, len(self.values)):
  1796. value = self.values[idx]
  1797. if value in elements:
  1798. values.append(value)
  1799. valuesDesc.append(self.valuesDesc[idx])
  1800. self.values = values
  1801. self.valuesDesc = valuesDesc
  1802. self.SetItems(self.valuesDesc)
  1803. def GetValue(self, name):
  1804. """Translate value
  1805. :param name: element name
  1806. """
  1807. idx = self.valuesDesc.index(name)
  1808. if idx > -1:
  1809. return self.values[idx]
  1810. return ''
  1811. class OgrTypeSelect(wx.Panel):
  1812. def __init__(self, parent, panel, **kwargs):
  1813. """Widget to choose OGR feature type
  1814. :param parent: parent window
  1815. :param panel: wx.Panel instance used as parent window
  1816. """
  1817. wx.Panel.__init__(self, parent = panel, id = wx.ID_ANY)
  1818. self.ftype = wx.Choice(parent = self, id = wx.ID_ANY,
  1819. size = (200, -1),
  1820. choices = (_("Point"), _("LineString"), _("Polygon")))
  1821. self._layout()
  1822. def _layout(self):
  1823. """Do layout"""
  1824. sizer = wx.BoxSizer(wx.HORIZONTAL)
  1825. sizer.Add(item = wx.StaticText(parent = self,
  1826. id = wx.ID_ANY,
  1827. label = _("Feature type:")),
  1828. proportion = 1,
  1829. flag = wx.ALIGN_CENTER_VERTICAL,
  1830. border = 5)
  1831. sizer.Add(item = self.ftype,
  1832. proportion = 0,
  1833. flag = wx.EXPAND | wx.ALIGN_RIGHT)
  1834. self.SetSizer(sizer)
  1835. sizer.Fit(self)
  1836. def GetType(self):
  1837. """Get selected type as string
  1838. :return: feature type as string
  1839. """
  1840. sel = self.ftype.GetSelection()
  1841. if sel == 0:
  1842. return 'point'
  1843. elif sel == 1:
  1844. return 'line'
  1845. elif sel == 2:
  1846. return 'boundary'
  1847. class CoordinatesSelect(wx.Panel):
  1848. def __init__(self, parent, giface, multiple = False, **kwargs):
  1849. """Widget to get coordinates from map window by mouse click
  1850. :param parent: parent window
  1851. :param giface: GRASS interface
  1852. :param multiple: - True if it is possible to insert more coordinates
  1853. """
  1854. self._giface = giface
  1855. self.multiple = multiple
  1856. self.mapWin = None
  1857. self.drawMapWin = None
  1858. super(CoordinatesSelect, self).__init__(parent=parent, id=wx.ID_ANY)
  1859. self.coordsField = wx.TextCtrl(parent=self, id=wx.ID_ANY,
  1860. size=globalvar.DIALOG_TEXTCTRL_SIZE,
  1861. validator=CoordinatesValidator())
  1862. icon = wx.Bitmap(os.path.join(globalvar.ICONDIR, "grass", "pointer.png"))
  1863. self.buttonInsCoords = buttons.ThemedGenBitmapToggleButton(parent=self, id=wx.ID_ANY,
  1864. bitmap=icon,
  1865. size=globalvar.DIALOG_COLOR_SIZE)
  1866. self.registered = False
  1867. self.buttonInsCoords.Bind(wx.EVT_BUTTON, self._onClick)
  1868. mapdisp = self._giface.GetMapDisplay()
  1869. if mapdisp:
  1870. switcher = mapdisp.GetToolSwitcher()
  1871. switcher.AddCustomToolToGroup(group='mouseUse',
  1872. btnId=self.buttonInsCoords.GetId(),
  1873. toggleHandler=self.buttonInsCoords.SetValue)
  1874. self._doLayout()
  1875. self.coordsField.Bind(wx.EVT_TEXT, lambda event : self._draw(delay=1))
  1876. def _doLayout(self):
  1877. self.dialogSizer = wx.BoxSizer(wx.HORIZONTAL)
  1878. self.dialogSizer.Add(item = self.coordsField,
  1879. proportion = 1,
  1880. flag = wx.EXPAND)
  1881. self.dialogSizer.Add(item = self.buttonInsCoords)
  1882. self.SetSizer(self.dialogSizer)
  1883. def _onClick(self, event):
  1884. """Button for interacitve inserting of coordinates clicked"""
  1885. self.mapWin = self._giface.GetMapWindow()
  1886. if self.buttonInsCoords.GetToggle() and self.mapWin:
  1887. switcher = self._giface.GetMapDisplay().GetToolSwitcher()
  1888. switcher.ToolChanged(self.buttonInsCoords.GetId())
  1889. if self.mapWin.RegisterMouseEventHandler(wx.EVT_LEFT_DOWN,
  1890. self._onMapClickHandler,
  1891. 'cross') == False:
  1892. return
  1893. self.registered = True
  1894. self._giface.GetMapDisplay().Raise()
  1895. else:
  1896. if self.mapWin and \
  1897. self.mapWin.UnregisterMouseEventHandler(wx.EVT_LEFT_DOWN,
  1898. self._onMapClickHandler):
  1899. self.registered = False
  1900. return
  1901. def drawCleanUp(self):
  1902. if self.drawMapWin:
  1903. self.drawMapWin.UnregisterGraphicsToDraw(self.pointsToDraw)
  1904. def _draw(self, delay):
  1905. """Draws points representing inserted coordinates in mapwindow."""
  1906. if self.drawMapWin != self.mapWin:
  1907. self.drawCleanUp()
  1908. if self.mapWin:
  1909. self.drawMapWin = self.mapWin
  1910. self.pointsToDraw = self.drawMapWin.RegisterGraphicsToDraw(graphicsType="point")
  1911. if self.drawMapWin:
  1912. items = self.pointsToDraw.GetAllItems()
  1913. for i in items:
  1914. self.pointsToDraw.DeleteItem(i)
  1915. coords = self._getCoords()
  1916. if coords is not None:
  1917. for i in range(len(coords)/2):
  1918. i = i * 2
  1919. self.pointsToDraw.AddItem(coords=(coords[i], coords[i + 1]))
  1920. self._giface.updateMap.emit(render=False, renderVector=False, delay=delay)
  1921. def _getCoords(self):
  1922. """Get list of coordinates.
  1923. :return: None if values are not valid
  1924. """
  1925. if self.coordsField.GetValidator().Validate():
  1926. return self.coordsField.GetValue().split(',')
  1927. return None
  1928. def _onMapClickHandler(self, event):
  1929. """Gets coordinates from mapwindow"""
  1930. if event == "unregistered":
  1931. return
  1932. e, n = self.mapWin.GetLastEN()
  1933. prevCoords = ""
  1934. if self.multiple:
  1935. prevCoords = self.coordsField.GetValue().strip()
  1936. if prevCoords != "":
  1937. prevCoords += ","
  1938. value = prevCoords + str(e) + "," + str(n)
  1939. self.coordsField.SetValue(value)
  1940. self._draw(delay=0)
  1941. def OnClose(self):
  1942. """Unregistrates _onMapClickHandler from mapWin"""
  1943. self.drawCleanUp()
  1944. self._giface.updateMap.emit(render=False, renderVector=False)
  1945. mapdisp = self._giface.GetMapDisplay()
  1946. if mapdisp:
  1947. switcher = mapdisp.GetToolSwitcher()
  1948. switcher.RemoveCustomToolFromGroup(self.buttonInsCoords.GetId())
  1949. if self.mapWin and self.registered:
  1950. self.mapWin.UnregisterMouseEventHandler(wx.EVT_LEFT_DOWN,
  1951. self._onMapClickHandler)
  1952. def GetTextWin(self):
  1953. """Get TextCtrl widget"""
  1954. return self.coordsField
  1955. class SignatureSelect(wx.ComboBox):
  1956. """Widget for selecting signatures"""
  1957. def __init__(self, parent, element, id = wx.ID_ANY, size = globalvar.DIALOG_GSELECT_SIZE,
  1958. **kwargs):
  1959. super(SignatureSelect, self).__init__(parent, id, size = size,
  1960. **kwargs)
  1961. self.element = element
  1962. self.SetName("SignatureSelect")
  1963. def Insert(self, group, subgroup = None):
  1964. """Insert signatures for defined group/subgroup
  1965. :param group: group name (can be fully-qualified)
  1966. :param subgroup: non fully-qualified name of subgroup
  1967. """
  1968. if not group:
  1969. return
  1970. gisenv = grass.gisenv()
  1971. try:
  1972. name, mapset = group.split('@', 1)
  1973. except ValueError:
  1974. name = group
  1975. mapset = gisenv['MAPSET']
  1976. path = os.path.join(gisenv['GISDBASE'], gisenv['LOCATION_NAME'], mapset,
  1977. 'group', name)
  1978. if subgroup:
  1979. path = os.path.join(path, 'subgroup', subgroup)
  1980. try:
  1981. items = list()
  1982. for element in os.listdir(os.path.join(path, self.element)):
  1983. items.append(element)
  1984. self.SetItems(items)
  1985. except OSError:
  1986. self.SetItems([])
  1987. self.SetValue('')
  1988. class SeparatorSelect(wx.ComboBox):
  1989. """Widget for selecting seperator"""
  1990. def __init__(self, parent, id = wx.ID_ANY, size = globalvar.DIALOG_GSELECT_SIZE,
  1991. **kwargs):
  1992. super(SeparatorSelect, self).__init__(parent, id, size = size,
  1993. **kwargs)
  1994. self.SetName("SeparatorSelect")
  1995. self.SetItems(['pipe', 'comma', 'space', 'tab', 'newline'])