dialogs.py 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117
  1. """
  2. @package web_services.dialogs
  3. @brief Dialogs for web services.
  4. List of classes:
  5. - dialogs::WSDialogBase
  6. - dialogs::AddWSDialog
  7. - dialogs::WSPropertiesDialog
  8. - dialogs::SaveWMSLayerDialog
  9. (C) 2009-2021 by the GRASS Development Team
  10. This program is free software under the GNU General Public License
  11. (>=v2). Read the file COPYING that comes with GRASS for details.
  12. @author Martin Landa <landa.martin gmail.com>
  13. @author Stepan Turek <stepan.turek seznam.cz>
  14. """
  15. import wx
  16. import os
  17. import six
  18. import shutil
  19. from copy import deepcopy
  20. import grass.script as grass
  21. from grass.script.task import cmdlist_to_tuple, cmdtuple_to_list
  22. from core import globalvar
  23. from core.debug import Debug
  24. from core.gcmd import GMessage, GWarning, GError
  25. from core.utils import GetSettingsPath
  26. from core.gconsole import CmdThread, GStderr, EVT_CMD_DONE, EVT_CMD_OUTPUT
  27. from gui_core.gselect import Select
  28. from gui_core.wrap import Button, StaticText, StaticBox, TextCtrl, RadioButton
  29. from web_services.widgets import WSPanel, WSManageSettingsWidget
  30. class WSDialogBase(wx.Dialog):
  31. """Base class for web service dialogs."""
  32. def __init__(
  33. self,
  34. parent,
  35. id=wx.ID_ANY,
  36. style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER,
  37. **kwargs,
  38. ):
  39. wx.Dialog.__init__(self, parent, id, style=style, **kwargs)
  40. self.parent = parent
  41. # contains panel for every web service on server
  42. self.ws_panels = {
  43. "WMS_1.1.1": {"panel": None, "label": "WMS 1.1.1"},
  44. "WMS_1.3.0": {"panel": None, "label": "WMS 1.3.0"},
  45. "WMTS": {"panel": None, "label": "WMTS"},
  46. "OnEarth": {"panel": None, "label": "OnEarth"},
  47. }
  48. # TODO: should be in file
  49. self.default_servers = {
  50. "OSM-WMS": [
  51. "https://ows.terrestris.de/osm/service?",
  52. "",
  53. "",
  54. ],
  55. "NASA GIBS WMTS": [
  56. "http://gibs.earthdata.nasa.gov/wmts/epsg4326/best/wmts.cgi",
  57. "",
  58. "",
  59. ],
  60. "tiles.maps.eox.at (Sentinel-2)": [
  61. "https://tiles.maps.eox.at/wms",
  62. "",
  63. "",
  64. ],
  65. }
  66. # holds reference to web service panel which is showed
  67. self.active_ws_panel = None
  68. # buttons which are disabled when the dialog is not connected
  69. self.run_btns = []
  70. # stores error messages for GError dialog showed when all web service
  71. # connections were unsuccessful
  72. self.error_msgs = ""
  73. self._createWidgets()
  74. self._doLayout()
  75. def _createWidgets(self):
  76. settingsFile = os.path.join(GetSettingsPath(), "wxWS")
  77. self.settsManager = WSManageSettingsWidget(
  78. parent=self, settingsFile=settingsFile, default_servers=self.default_servers
  79. )
  80. self.settingsBox = StaticBox(
  81. parent=self, id=wx.ID_ANY, label=_(" Server settings ")
  82. )
  83. self.serverText = StaticText(parent=self, id=wx.ID_ANY, label=_("Server:"))
  84. self.server = TextCtrl(parent=self, id=wx.ID_ANY)
  85. self.btn_connect = Button(parent=self, id=wx.ID_ANY, label=_("&Connect"))
  86. self.btn_connect.SetToolTip(_("Connect to the server"))
  87. if not self.server.GetValue():
  88. self.btn_connect.Enable(False)
  89. self.infoCollapseLabelExp = _("Show advanced connection settings")
  90. self.infoCollapseLabelCol = _("Hide advanced connection settings")
  91. self.adv_conn = wx.CollapsiblePane(
  92. parent=self,
  93. label=self.infoCollapseLabelExp,
  94. style=wx.CP_DEFAULT_STYLE | wx.CP_NO_TLW_RESIZE | wx.EXPAND,
  95. )
  96. self.MakeAdvConnPane(pane=self.adv_conn.GetPane())
  97. self.adv_conn.Collapse(True)
  98. self.Bind(
  99. wx.EVT_COLLAPSIBLEPANE_CHANGED, self.OnAdvConnPaneChanged, self.adv_conn
  100. )
  101. self.reqDataPanel = wx.Panel(parent=self, id=wx.ID_ANY)
  102. self.layerNameBox = StaticBox(
  103. parent=self.reqDataPanel, id=wx.ID_ANY, label=_(" Layer Manager Settings ")
  104. )
  105. self.layerNameText = StaticText(
  106. parent=self.reqDataPanel, id=wx.ID_ANY, label=_("Output layer name:")
  107. )
  108. self.layerName = TextCtrl(parent=self.reqDataPanel, id=wx.ID_ANY)
  109. for ws in six.iterkeys(self.ws_panels):
  110. # set class WSPanel argument layerNameTxtCtrl
  111. self.ws_panels[ws]["panel"] = WSPanel(
  112. parent=self.reqDataPanel, web_service=ws
  113. )
  114. self.ws_panels[ws]["panel"].capParsed.connect(self.OnPanelCapParsed)
  115. self.ws_panels[ws]["panel"].layerSelected.connect(self.OnLayerSelected)
  116. # buttons
  117. self.btn_close = Button(parent=self, id=wx.ID_CLOSE)
  118. self.btn_close.SetToolTip(_("Close dialog"))
  119. # statusbar
  120. self.statusbar = wx.StatusBar(parent=self, id=wx.ID_ANY)
  121. # bindings
  122. self.btn_close.Bind(wx.EVT_BUTTON, self.OnClose)
  123. self.Bind(wx.EVT_CLOSE, self.OnClose)
  124. self.btn_connect.Bind(wx.EVT_BUTTON, self.OnConnect)
  125. self.server.Bind(wx.EVT_TEXT, self.OnServer)
  126. self.layerName.Bind(wx.EVT_TEXT, self.OnOutputLayerName)
  127. self.settsManager.settingsChanged.connect(self.OnSettingsChanged)
  128. self.settsManager.settingsSaving.connect(self.OnSettingsSaving)
  129. def OnLayerSelected(self, title):
  130. self.layerName.SetValue(title)
  131. def _doLayout(self):
  132. dialogSizer = wx.BoxSizer(wx.VERTICAL)
  133. dialogSizer.Add(
  134. self.settsManager,
  135. proportion=0,
  136. flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT,
  137. border=5,
  138. )
  139. # connectin settings
  140. settingsSizer = wx.StaticBoxSizer(self.settingsBox, wx.VERTICAL)
  141. serverSizer = wx.FlexGridSizer(cols=3, vgap=5, hgap=5)
  142. serverSizer.Add(self.serverText, flag=wx.ALIGN_CENTER_VERTICAL)
  143. serverSizer.AddGrowableCol(1)
  144. serverSizer.Add(self.server, flag=wx.EXPAND | wx.ALL)
  145. serverSizer.Add(self.btn_connect)
  146. settingsSizer.Add(
  147. serverSizer, proportion=0, flag=wx.EXPAND | wx.LEFT | wx.RIGHT, border=5
  148. )
  149. settingsSizer.Add(self.adv_conn, flag=wx.ALL | wx.EXPAND, border=5)
  150. dialogSizer.Add(
  151. settingsSizer,
  152. proportion=0,
  153. flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT,
  154. border=5,
  155. )
  156. # layer name, parsed capabilities
  157. reqDataSizer = wx.BoxSizer(wx.VERTICAL)
  158. layerNameSizer = wx.StaticBoxSizer(self.layerNameBox, wx.HORIZONTAL)
  159. layerNameSizer.Add(
  160. self.layerNameText, flag=wx.ALIGN_CENTER_VERTICAL | wx.ALL, border=5
  161. )
  162. layerNameSizer.Add(self.layerName, flag=wx.EXPAND, proportion=1)
  163. reqDataSizer.Add(
  164. layerNameSizer, flag=wx.TOP | wx.LEFT | wx.RIGHT | wx.EXPAND, border=5
  165. )
  166. self.ch_ws_sizer = wx.BoxSizer(wx.VERTICAL)
  167. reqDataSizer.Add(
  168. self.ch_ws_sizer, proportion=0, flag=wx.TOP | wx.EXPAND, border=5
  169. )
  170. for ws in six.iterkeys(self.ws_panels):
  171. reqDataSizer.Add(
  172. self.ws_panels[ws]["panel"],
  173. proportion=1,
  174. flag=wx.TOP | wx.LEFT | wx.RIGHT | wx.EXPAND,
  175. border=5,
  176. )
  177. self.ws_panels[ws]["panel"].Hide()
  178. dialogSizer.Add(self.reqDataPanel, proportion=1, flag=wx.EXPAND)
  179. self.reqDataPanel.SetSizer(reqDataSizer)
  180. self.reqDataPanel.Hide()
  181. # buttons
  182. self.btnsizer = wx.BoxSizer(orient=wx.HORIZONTAL)
  183. self.btnsizer.Add(
  184. self.btn_close, proportion=0, flag=wx.ALL | wx.ALIGN_CENTER, border=10
  185. )
  186. dialogSizer.Add(self.btnsizer, proportion=0, flag=wx.ALIGN_CENTER)
  187. # expand wxWidget wx.StatusBar
  188. statusbarSizer = wx.BoxSizer(wx.HORIZONTAL)
  189. statusbarSizer.Add(self.statusbar, proportion=1, flag=wx.EXPAND)
  190. dialogSizer.Add(statusbarSizer, proportion=0, flag=wx.EXPAND)
  191. self.SetSizer(dialogSizer)
  192. self.Layout()
  193. self.SetMinSize((550, -1))
  194. self.SetMaxSize((-1, self.GetBestSize()[1]))
  195. self.Fit()
  196. def MakeAdvConnPane(self, pane):
  197. """Create advanced connection settings pane"""
  198. self.usernameText = StaticText(parent=pane, id=wx.ID_ANY, label=_("Username:"))
  199. self.username = TextCtrl(parent=pane, id=wx.ID_ANY)
  200. self.passwText = StaticText(parent=pane, id=wx.ID_ANY, label=_("Password:"))
  201. self.password = TextCtrl(parent=pane, id=wx.ID_ANY, style=wx.TE_PASSWORD)
  202. # pane layout
  203. adv_conn_sizer = wx.BoxSizer(wx.VERTICAL)
  204. usernameSizer = wx.BoxSizer(wx.HORIZONTAL)
  205. usernameSizer.Add(self.usernameText, flag=wx.ALIGN_CENTER_VERTICAL, border=5)
  206. usernameSizer.Add(self.username, proportion=1, flag=wx.EXPAND, border=5)
  207. adv_conn_sizer.Add(usernameSizer, flag=wx.ALL | wx.EXPAND, border=5)
  208. passwSizer = wx.BoxSizer(wx.HORIZONTAL)
  209. passwSizer.Add(self.passwText, flag=wx.ALIGN_CENTER_VERTICAL, border=5)
  210. passwSizer.Add(self.password, proportion=1, flag=wx.EXPAND, border=5)
  211. adv_conn_sizer.Add(passwSizer, flag=wx.ALL | wx.EXPAND, border=5)
  212. pane.SetSizer(adv_conn_sizer)
  213. adv_conn_sizer.Fit(pane)
  214. pane.SetSizer(adv_conn_sizer)
  215. adv_conn_sizer.Fit(pane)
  216. def OnSettingsSaving(self, name):
  217. """Check if required data are filled before setting save is performed."""
  218. server = self.server.GetValue().strip()
  219. if not server:
  220. GMessage(
  221. parent=self,
  222. message=_("No data source defined, settings are not saved."),
  223. )
  224. return
  225. self.settsManager.SetDataToSave(
  226. (server, self.username.GetValue(), self.password.GetValue())
  227. )
  228. self.settsManager.SaveSettings(name)
  229. def OnSettingsChanged(self, data):
  230. """Update widgets according to chosen settings"""
  231. # data list: [server, username, password]
  232. if len(data) < 3:
  233. return
  234. self.server.SetValue(data[0])
  235. self.username.SetValue(data[1])
  236. self.password.SetValue(data[2])
  237. if data[1] or data[2]:
  238. self.adv_conn.Expand()
  239. else:
  240. self.adv_conn.Collapse(True)
  241. # clear content of the wxWidget wx.TextCtrl (Output layer
  242. # name:), based on changing default server selection in the
  243. # wxWidget wx.Choice
  244. if len(self.layerName.GetValue()) > 0:
  245. self.layerName.Clear()
  246. def OnClose(self, event):
  247. """Close the dialog"""
  248. """Close dialog"""
  249. if not self.IsModal():
  250. self.Destroy()
  251. event.Skip()
  252. def _getCapFiles(self):
  253. ws_cap_files = {}
  254. for v in six.itervalues(self.ws_panels):
  255. ws_cap_files[v["panel"].GetWebService()] = v["panel"].GetCapFile()
  256. return ws_cap_files
  257. def OnServer(self, event):
  258. """Server settings edited"""
  259. value = event.GetString()
  260. if value:
  261. self.btn_connect.Enable(True)
  262. else:
  263. self.btn_connect.Enable(False)
  264. # clear content of the wxWidget wx.TextCtrl (Output Layer
  265. # name:), based on changing content of the wxWidget
  266. # wx.TextCtrl (Server:)
  267. self.layerName.Clear()
  268. def OnOutputLayerName(self, event):
  269. """Update layer name to web service panel"""
  270. lname = event.GetString()
  271. for v in six.itervalues(self.ws_panels):
  272. v["panel"].SetOutputLayerName(lname.strip())
  273. def OnConnect(self, event):
  274. """Connect to the server"""
  275. server = self.server.GetValue().strip()
  276. self.ch_ws_sizer.Clear(True)
  277. if self.active_ws_panel is not None:
  278. self.reqDataPanel.Hide()
  279. for btn in self.run_btns:
  280. btn.Enable(False)
  281. self.active_ws_panel = None
  282. self.Layout()
  283. self.Fit()
  284. self.statusbar.SetStatusText(
  285. _("Connecting to <%s>..." % self.server.GetValue().strip())
  286. )
  287. # number of panels already connected
  288. self.finished_panels_num = 0
  289. for ws in six.iterkeys(self.ws_panels):
  290. self.ws_panels[ws]["panel"].ConnectToServer(
  291. url=server,
  292. username=self.username.GetValue(),
  293. password=self.password.GetValue(),
  294. )
  295. self.ws_panels[ws]["panel"].Hide()
  296. def OnPanelCapParsed(self, error_msg):
  297. """Called when panel has downloaded and parsed capabilities file."""
  298. # how many web service panels are finished
  299. self.finished_panels_num += 1
  300. if error_msg:
  301. self.error_msgs += "\n" + error_msg
  302. # if all are finished, show panels, which succeeded in connection
  303. if self.finished_panels_num == len(self.ws_panels):
  304. self.UpdateDialogAfterConnection()
  305. # show error dialog only if connections to all web services were
  306. # unsuccessful
  307. if not self._getConnectedWS() and self.error_msgs:
  308. GError(self.error_msgs, parent=self)
  309. self.error_msgs = ""
  310. self.Layout()
  311. self.Fit()
  312. def _getConnectedWS(self):
  313. """
  314. :return: list of found web services on server (identified as keys in self.ws_panels)
  315. """
  316. conn_ws = []
  317. for ws, data in six.iteritems(self.ws_panels):
  318. if data["panel"].IsConnected():
  319. conn_ws.append(ws)
  320. return conn_ws
  321. def UpdateDialogAfterConnection(self):
  322. """Update dialog after all web service panels downloaded and parsed capabilities data."""
  323. avail_ws = {}
  324. conn_ws = self._getConnectedWS()
  325. for ws in conn_ws:
  326. avail_ws[ws] = self.ws_panels[ws]
  327. self.web_service_sel = []
  328. self.rb_choices = []
  329. # at least one web service found on server
  330. if len(avail_ws) > 0:
  331. self.reqDataPanel.Show()
  332. self.rb_order = ["WMS_1.1.1", "WMS_1.3.0", "WMTS", "OnEarth"]
  333. for ws in self.rb_order:
  334. if ws in avail_ws:
  335. self.web_service_sel.append(ws)
  336. self.rb_choices.append(avail_ws[ws]["label"])
  337. self.choose_ws_rb = wx.RadioBox(
  338. parent=self.reqDataPanel,
  339. id=wx.ID_ANY,
  340. label=_("Available web services"),
  341. pos=wx.DefaultPosition,
  342. choices=self.rb_choices,
  343. majorDimension=1,
  344. style=wx.RA_SPECIFY_ROWS,
  345. )
  346. self.Bind(wx.EVT_RADIOBOX, self.OnChooseWs, self.choose_ws_rb)
  347. self.ch_ws_sizer.Add(
  348. self.choose_ws_rb,
  349. flag=wx.TOP | wx.LEFT | wx.RIGHT | wx.EXPAND,
  350. border=5,
  351. )
  352. self._showWsPanel(self.web_service_sel[self.choose_ws_rb.GetSelection()])
  353. self.statusbar.SetStatusText(
  354. _("Connected to <%s>" % self.server.GetValue().strip())
  355. )
  356. for btn in self.run_btns:
  357. btn.Enable(True)
  358. # no web service found on server
  359. else:
  360. self.statusbar.SetStatusText(
  361. _("Unable to connect to <%s>" % self.server.GetValue().strip())
  362. )
  363. for btn in self.run_btns:
  364. btn.Enable(False)
  365. self.reqDataPanel.Hide()
  366. self.active_ws_panel = None
  367. def OnChooseWs(self, event):
  368. """Show panel corresponding to selected web service."""
  369. chosen_r = event.GetInt()
  370. self._showWsPanel(self.web_service_sel[chosen_r])
  371. def _showWsPanel(self, ws):
  372. """Helper function"""
  373. if self.active_ws_panel is not None:
  374. self.active_ws_panel.Hide()
  375. self.active_ws_panel = self.ws_panels[ws]["panel"]
  376. if not self.active_ws_panel.IsShown():
  377. self.active_ws_panel.Show()
  378. self.SetMaxSize((-1, -1))
  379. self.active_ws_panel.GetContainingSizer().Layout()
  380. def OnAdvConnPaneChanged(self, event):
  381. """Collapse search module box"""
  382. if self.adv_conn.IsExpanded():
  383. self.adv_conn.SetLabel(self.infoCollapseLabelCol)
  384. else:
  385. self.adv_conn.SetLabel(self.infoCollapseLabelExp)
  386. self.Layout()
  387. self.SetMaxSize((-1, self.GetBestSize()[1]))
  388. self.SendSizeEvent()
  389. self.Fit()
  390. class AddWSDialog(WSDialogBase):
  391. """Dialog for adding web service layer."""
  392. def __init__(
  393. self,
  394. parent,
  395. giface,
  396. id=wx.ID_ANY,
  397. style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER,
  398. **kwargs,
  399. ):
  400. WSDialogBase.__init__(
  401. self,
  402. parent,
  403. id=wx.ID_ANY,
  404. style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER,
  405. **kwargs,
  406. )
  407. self.SetTitle(_("Add web service layer"))
  408. self.parent = parent
  409. self.giface = giface
  410. self.btn_connect.SetDefault()
  411. def _createWidgets(self):
  412. WSDialogBase._createWidgets(self)
  413. self.btn_add = Button(parent=self, id=wx.ID_ANY, label=_("&Add layer"))
  414. self.btn_add.SetToolTip(
  415. _("Add selected web service layers as map layer into layer tree")
  416. )
  417. self.btn_add.Enable(False)
  418. self.run_btns.append(self.btn_add)
  419. def _doLayout(self):
  420. WSDialogBase._doLayout(self)
  421. self.btnsizer.Add(
  422. self.btn_add, proportion=0, flag=wx.ALL | wx.ALIGN_CENTER, border=10
  423. )
  424. # bindings
  425. self.btn_add.Bind(wx.EVT_BUTTON, self.OnAddLayer)
  426. def UpdateDialogAfterConnection(self):
  427. """Connect to the server"""
  428. WSDialogBase.UpdateDialogAfterConnection(self)
  429. if self._getConnectedWS():
  430. self.btn_add.SetDefault()
  431. else:
  432. self.btn_connect.SetDefault()
  433. def OnAddLayer(self, event):
  434. """Add web service layer."""
  435. # add layer
  436. if self.active_ws_panel is None:
  437. return
  438. lcmd = self.active_ws_panel.CreateCmd()
  439. if not lcmd:
  440. return None
  441. # TODO: It is not clear how to do GetOptData in giface
  442. # knowing what GetOptData is doing might help
  443. # (maybe Get... is not the right name)
  444. # please fix giface if you know
  445. # tree -> giface
  446. # GetLayerTree -> GetLayerList
  447. # AddLayer -> AddLayer (but tree ones returns some layer,
  448. # giface ones nothing)
  449. # GetLayerInfo -> Layer object can by used instead
  450. # GetOptData -> unknown
  451. ltree = self.giface.GetLayerTree()
  452. active_ws = self.active_ws_panel.GetWebService()
  453. if "WMS" not in active_ws:
  454. cap_file = self.active_ws_panel.GetCapFile()
  455. cmd_cap_file = grass.tempfile()
  456. shutil.copyfile(cap_file, cmd_cap_file)
  457. lcmd.append("capfile=" + cmd_cap_file)
  458. layer = ltree.AddLayer(
  459. ltype="wms",
  460. lname=self.active_ws_panel.GetOutputLayerName(),
  461. lchecked=True,
  462. lcmd=lcmd,
  463. )
  464. ws_cap_files = self._getCapFiles()
  465. # create properties dialog
  466. cmd_list = ltree.GetLayerInfo(layer, "cmd")
  467. cmd = cmdlist_to_tuple(cmd_list)
  468. prop_win = WSPropertiesDialog(
  469. parent=self.parent,
  470. giface=self.giface,
  471. id=wx.ID_ANY,
  472. layer=layer,
  473. ws_cap_files=ws_cap_files,
  474. cmd=cmd,
  475. )
  476. prop_win.Hide()
  477. ltree.GetOptData(dcmd=None, layer=layer, params=None, propwin=prop_win)
  478. class WSPropertiesDialog(WSDialogBase):
  479. """Dialog for editing web service properties."""
  480. def __init__(
  481. self,
  482. parent,
  483. giface,
  484. layer,
  485. ws_cap_files,
  486. cmd,
  487. id=wx.ID_ANY,
  488. style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER,
  489. **kwargs,
  490. ):
  491. """
  492. :param giface: grass interface
  493. :param layer: layer tree item
  494. :param ws_cap_files: dict web service('WMS_1.1.1', 'WMS_1.3.0',
  495. 'WMTS', 'OnEarth') : cap file path cap files, which will be parsed
  496. :param cmd: cmd to which dialog widgets will be initialized if
  497. it is possible (cmp parameters exists in parsed web service cap_file)
  498. """
  499. WSDialogBase.__init__(
  500. self,
  501. parent,
  502. id=wx.ID_ANY,
  503. style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER,
  504. **kwargs,
  505. )
  506. self.SetTitle(_("Web service layer properties"))
  507. self.layer = layer
  508. self.giface = giface
  509. # after web service panels are connected, set dialog widgets
  510. # according to cmd in this variable (if it is not None)
  511. self.cmd_to_set = None
  512. # store data needed for reverting
  513. self.revert_ws_cap_files = {}
  514. self.revert_cmd = cmd
  515. ws_cap = self._getWSfromCmd(cmd)
  516. for ws in six.iterkeys(self.ws_panels):
  517. # cap file used in cmd will be deleted, thnaks to the dialogs
  518. # destructor
  519. if ws == ws_cap and "capfile" in cmd[1]:
  520. self.revert_ws_cap_files[ws] = cmd[1]["capfile"]
  521. del ws_cap_files[ws]
  522. else:
  523. self.revert_ws_cap_files[ws] = grass.tempfile()
  524. self._setRevertCapFiles(ws_cap_files)
  525. self.LoadCapFiles(ws_cap_files=self.revert_ws_cap_files, cmd=cmd)
  526. self.btn_ok.SetDefault()
  527. def __del__(self):
  528. for f in six.itervalues(self.revert_ws_cap_files):
  529. grass.try_remove(f)
  530. def _setRevertCapFiles(self, ws_cap_files):
  531. for ws, f in six.iteritems(ws_cap_files):
  532. if os.path.isfile(ws_cap_files[ws]):
  533. shutil.copyfile(f, self.revert_ws_cap_files[ws])
  534. else:
  535. # delete file content
  536. f_o = open(f, "w")
  537. f_o.close()
  538. def _createWidgets(self):
  539. WSDialogBase._createWidgets(self)
  540. self.btn_apply = Button(parent=self, id=wx.ID_ANY, label=_("&Apply"))
  541. self.btn_apply.SetToolTip(_("Apply changes"))
  542. self.btn_apply.Enable(False)
  543. self.run_btns.append(self.btn_apply)
  544. self.btn_ok = Button(parent=self, id=wx.ID_ANY, label=_("&OK"))
  545. self.btn_ok.SetToolTip(_("Apply changes and close dialog"))
  546. self.btn_ok.Enable(False)
  547. self.run_btns.append(self.btn_ok)
  548. def _doLayout(self):
  549. WSDialogBase._doLayout(self)
  550. self.btnsizer.Add(
  551. self.btn_apply, proportion=0, flag=wx.ALL | wx.ALIGN_CENTER, border=10
  552. )
  553. self.btnsizer.Add(
  554. self.btn_ok, proportion=0, flag=wx.ALL | wx.ALIGN_CENTER, border=10
  555. )
  556. # bindings
  557. self.btn_apply.Bind(wx.EVT_BUTTON, self.OnApply)
  558. self.btn_ok.Bind(wx.EVT_BUTTON, self.OnSave)
  559. def LoadCapFiles(self, ws_cap_files, cmd):
  560. """Parse cap files and update dialog.
  561. For parameters description, see the constructor.
  562. """
  563. self.ch_ws_sizer.Clear(True)
  564. self.cmd_to_set = cmd
  565. self.finished_panels_num = 0
  566. conn = self._getServerConnFromCmd(cmd)
  567. self.server.SetValue(conn["url"])
  568. self.password.SetValue(conn["password"])
  569. self.username.SetValue(conn["username"])
  570. self.layerName.SetValue(cmd[1]["map"])
  571. for ws, data in six.iteritems(self.ws_panels):
  572. cap_file = None
  573. if ws in ws_cap_files:
  574. cap_file = ws_cap_files[ws]
  575. data["panel"].ParseCapFile(
  576. url=conn["url"],
  577. username=conn["password"],
  578. password=conn["username"],
  579. cap_file=cap_file,
  580. )
  581. def _getServerConnFromCmd(self, cmd):
  582. """Get url/server/passwod from cmd tuple"""
  583. conn = {"url": "", "username": "", "password": ""}
  584. for k in six.iterkeys(conn):
  585. if k in cmd[1]:
  586. conn[k] = cmd[1][k]
  587. return conn
  588. def _apply(self):
  589. """Apply chosen values from widgets to web service layer."""
  590. lcmd = self.active_ws_panel.CreateCmd()
  591. if not lcmd:
  592. return
  593. active_ws = self.active_ws_panel.GetWebService()
  594. if "WMS" not in active_ws:
  595. lcmd.append("capfile=" + self.revert_ws_cap_files[active_ws])
  596. self.giface.GetLayerTree().GetOptData(
  597. dcmd=lcmd, layer=self.layer, params=True, propwin=self
  598. )
  599. # TODO use just list or tuple
  600. cmd = cmdlist_to_tuple(lcmd)
  601. self.revert_cmd = cmd
  602. self._setRevertCapFiles(self._getCapFiles())
  603. self.giface.updateMap.emit()
  604. def UpdateDialogAfterConnection(self):
  605. """Connect to the server"""
  606. WSDialogBase.UpdateDialogAfterConnection(self)
  607. if self._getConnectedWS():
  608. self.btn_ok.SetDefault()
  609. else:
  610. self.btn_connect.SetDefault()
  611. def OnApply(self, event):
  612. self._apply()
  613. def OnSave(self, event):
  614. self._apply()
  615. self._close()
  616. def OnClose(self, event):
  617. """Close dialog"""
  618. self._close()
  619. def _close(self):
  620. """Hide dialog"""
  621. self.Hide()
  622. self.LoadCapFiles(cmd=self.revert_cmd, ws_cap_files=self.revert_ws_cap_files)
  623. def OnPanelCapParsed(self, error_msg):
  624. """Called when panel has downloaded and parsed capabilities file."""
  625. WSDialogBase.OnPanelCapParsed(self, error_msg)
  626. if self.finished_panels_num == len(self.ws_panels):
  627. if self.cmd_to_set:
  628. self._updateWsPanelWidgetsByCmd(self.cmd_to_set)
  629. self.cmd_to_set = None
  630. def _updateWsPanelWidgetsByCmd(self, cmd):
  631. """Set values of widgets according to parameters in cmd."""
  632. ws = self._getWSfromCmd(cmd)
  633. if self.ws_panels[ws]["panel"].IsConnected():
  634. self.choose_ws_rb.SetStringSelection(self.ws_panels[ws]["label"])
  635. self._showWsPanel(ws)
  636. self.ws_panels[ws]["panel"].UpdateWidgetsByCmd(cmd)
  637. def _getWSfromCmd(self, cmd):
  638. driver = cmd[1]["driver"]
  639. ws = driver.split("_")[0]
  640. if ws == "WMS":
  641. ws += "_" + cmd[1]["wms_version"]
  642. return ws
  643. class SaveWMSLayerDialog(wx.Dialog):
  644. """Dialog for saving web service layer into GRASS vector/raster layer.
  645. .. todo::
  646. Implement saving data in region of map display.
  647. """
  648. def __init__(self, parent, layer, giface):
  649. wx.Dialog.__init__(
  650. self,
  651. parent=parent,
  652. title=("Save web service layer as raster map"),
  653. id=wx.ID_ANY,
  654. )
  655. self.layer = layer
  656. self._giface = giface
  657. self.cmd = self.layer.GetCmd()
  658. self.thread = CmdThread(self)
  659. self.cmdStdErr = GStderr(self)
  660. self._createWidgets()
  661. def _createWidgets(self):
  662. self.labels = {}
  663. self.params = {}
  664. self.labels["output"] = StaticText(
  665. parent=self, id=wx.ID_ANY, label=_("Name for output raster map:")
  666. )
  667. self.params["output"] = Select(
  668. parent=self,
  669. type="raster",
  670. mapsets=[grass.gisenv()["MAPSET"]],
  671. size=globalvar.DIALOG_GSELECT_SIZE,
  672. )
  673. self.regionStBoxLabel = StaticBox(
  674. parent=self, id=wx.ID_ANY, label=" %s " % _("Export region")
  675. )
  676. self.region_types_order = ["display", "comp", "named"]
  677. self.region_types = {}
  678. self.region_types["display"] = RadioButton(
  679. parent=self, label=_("Map display"), style=wx.RB_GROUP
  680. )
  681. self.region_types["comp"] = RadioButton(
  682. parent=self, label=_("Computational region")
  683. )
  684. self.region_types["named"] = RadioButton(parent=self, label=_("Named region"))
  685. self.region_types["display"].SetToolTip(
  686. _("Extent and resolution" " are based on Map Display geometry.")
  687. )
  688. self.region_types["comp"].SetToolTip(
  689. _("Extent and resolution" " are based on computational region.")
  690. )
  691. self.region_types["named"].SetToolTip(
  692. _("Extent and resolution" " are based on named region.")
  693. )
  694. self.region_types["display"].SetValue(True) # set default as map display
  695. self.overwrite = wx.CheckBox(
  696. parent=self, id=wx.ID_ANY, label=_("Overwrite existing raster map")
  697. )
  698. self.named_reg_panel = wx.Panel(parent=self, id=wx.ID_ANY)
  699. self.labels["region"] = StaticText(
  700. parent=self.named_reg_panel, id=wx.ID_ANY, label=_("Choose named region:")
  701. )
  702. self.params["region"] = Select(
  703. parent=self.named_reg_panel,
  704. type="region",
  705. size=globalvar.DIALOG_GSELECT_SIZE,
  706. )
  707. # buttons
  708. self.btn_close = Button(parent=self, id=wx.ID_CLOSE)
  709. self.SetEscapeId(self.btn_close.GetId())
  710. self.btn_close.SetToolTip(_("Close dialog"))
  711. self.btn_ok = Button(parent=self, label=_("&Save layer"))
  712. self.btn_ok.SetToolTip(_("Save web service layer as raster map"))
  713. # statusbar
  714. self.statusbar = wx.StatusBar(parent=self, id=wx.ID_ANY)
  715. self._layout()
  716. def _layout(self):
  717. self._border = wx.BoxSizer(wx.VERTICAL)
  718. dialogSizer = wx.BoxSizer(wx.VERTICAL)
  719. regionSizer = wx.BoxSizer(wx.HORIZONTAL)
  720. dialogSizer.Add(
  721. self._addSelectSizer(title=self.labels["output"], sel=self.params["output"])
  722. )
  723. regionSizer = wx.StaticBoxSizer(self.regionStBoxLabel, wx.VERTICAL)
  724. regionTypeSizer = wx.BoxSizer(wx.HORIZONTAL)
  725. for r_type in self.region_types_order:
  726. regionTypeSizer.Add(self.region_types[r_type], flag=wx.RIGHT, border=8)
  727. regionSizer.Add(regionTypeSizer)
  728. self.named_reg_panel.SetSizer(
  729. self._addSelectSizer(title=self.labels["region"], sel=self.params["region"])
  730. )
  731. regionSizer.Add(self.named_reg_panel)
  732. self.named_reg_panel.Hide()
  733. dialogSizer.Add(regionSizer, flag=wx.EXPAND)
  734. dialogSizer.Add(self.overwrite, flag=wx.TOP, border=10)
  735. # buttons
  736. self.btnsizer = wx.BoxSizer(orient=wx.HORIZONTAL)
  737. self.btnsizer.Add(
  738. self.btn_close, proportion=0, flag=wx.ALL | wx.ALIGN_CENTER, border=10
  739. )
  740. self.btnsizer.Add(
  741. self.btn_ok, proportion=0, flag=wx.ALL | wx.ALIGN_CENTER, border=10
  742. )
  743. dialogSizer.Add(self.btnsizer, proportion=0, flag=wx.ALIGN_CENTER)
  744. self._border.Add(dialogSizer, proportion=0, flag=wx.ALL, border=5)
  745. self._border.Add(self.statusbar, proportion=0)
  746. self.SetSizer(self._border)
  747. self.Layout()
  748. self.Fit()
  749. # bindings
  750. self.btn_ok.Bind(wx.EVT_BUTTON, self.OnSave)
  751. self.Bind(EVT_CMD_DONE, self.OnCmdDone)
  752. self.Bind(EVT_CMD_OUTPUT, self.OnCmdOutput)
  753. for r_type in self.region_types_order:
  754. self.Bind(wx.EVT_RADIOBUTTON, self.OnRegionType, self.region_types[r_type])
  755. def _addSelectSizer(self, title, sel):
  756. """Helper layout function."""
  757. selSizer = wx.BoxSizer(orient=wx.VERTICAL)
  758. selTitleSizer = wx.BoxSizer(wx.HORIZONTAL)
  759. selTitleSizer.Add(
  760. title, proportion=1, flag=wx.LEFT | wx.TOP | wx.EXPAND, border=5
  761. )
  762. selSizer.Add(selTitleSizer, proportion=0, flag=wx.EXPAND)
  763. selSizer.Add(
  764. sel,
  765. proportion=1,
  766. flag=wx.EXPAND | wx.ALL,
  767. border=5,
  768. )
  769. return selSizer
  770. def OnRegionType(self, event):
  771. selected = event.GetEventObject()
  772. if selected == self.region_types["named"]:
  773. self.named_reg_panel.Show()
  774. else:
  775. self.named_reg_panel.Hide()
  776. self._border.Layout()
  777. self.Fit()
  778. def OnSave(self, event):
  779. """Import WMS raster data into GRASS as raster layer."""
  780. self.thread.abort(abortall=True)
  781. currmapset = grass.gisenv()["MAPSET"]
  782. self.output = self.params["output"].GetValue().strip()
  783. l_spl = self.output.strip().split("@")
  784. # check output layer
  785. msg = None
  786. if not self.output:
  787. msg = _("Missing output raster.")
  788. elif len(l_spl) > 1 and l_spl[1] != currmapset:
  789. msg = _("Output map can be added only to current mapset.")
  790. elif (
  791. not self.overwrite.IsChecked()
  792. and grass.find_file(self.output, "cell", ".")["fullname"]
  793. ):
  794. msg = _("Output map <%s> already exists" % self.output)
  795. if msg:
  796. GMessage(parent=self, message=msg)
  797. return
  798. self.output = l_spl[0]
  799. # check region
  800. region = self.params["region"].GetValue().strip()
  801. reg_spl = region.strip().split("@")
  802. reg_mapset = "."
  803. if len(reg_spl) > 1:
  804. reg_mapset = reg_spl[1]
  805. if self.region_types["named"].GetValue():
  806. if not grass.find_file(reg_spl[0], "windows", reg_mapset)["fullname"]:
  807. msg = _(
  808. "Region <%s> does not exist." % self.params["region"].GetValue()
  809. )
  810. GWarning(parent=self, message=msg)
  811. return
  812. # create r.in.wms command
  813. cmd = ("r.in.wms", deepcopy(self.cmd[1]))
  814. if "map" in cmd[1]:
  815. del cmd[1]["map"]
  816. cmd[1]["output"] = self.output
  817. if self.overwrite.IsChecked():
  818. cmd[1]["overwrite"] = True
  819. env = os.environ.copy()
  820. if self.region_types["named"].GetValue():
  821. cmd[1]["region"] = region
  822. elif self.region_types["display"].GetValue():
  823. region = self._giface.GetMapWindow().GetMap().SetRegion()
  824. env["GRASS_REGION"] = region
  825. cmdList = cmdtuple_to_list(cmd)
  826. self.currentPid = self.thread.GetId()
  827. self.thread.RunCmd(cmdList, env=env, stderr=self.cmdStdErr)
  828. self.statusbar.SetStatusText(_("Downloading data..."))
  829. def OnCmdDone(self, event):
  830. """When data are fetched."""
  831. if event.pid != self.currentPid:
  832. return
  833. self._addLayer()
  834. self.statusbar.SetStatusText("")
  835. def _addLayer(self):
  836. """Add layer into layer tree."""
  837. llist = self._giface.GetLayerList()
  838. if len(llist.GetLayersByName(self.output)) == 0:
  839. cmd = ["d.rast", "map=" + self.output]
  840. llist.AddLayer(ltype="raster", name=self.output, cmd=cmd, checked=True)
  841. def OnCmdOutput(self, event):
  842. """Handle cmd output according to debug level."""
  843. if Debug.GetLevel() == 0:
  844. if event.type == "error":
  845. msg = _("Unable to fetch data.\n")
  846. msg += event.text
  847. GWarning(parent=self, message=msg)
  848. else:
  849. Debug.msg(1, event.text)