maindialog.py 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018
  1. ###############################################################################
  2. #
  3. # CSW Client
  4. # ---------------------------------------------------------
  5. # QGIS Catalog Service client.
  6. #
  7. # Copyright (C) 2010 NextGIS (http://nextgis.org),
  8. # Alexander Bruy (alexander.bruy@gmail.com),
  9. # Maxim Dubinin (sim@gis-lab.info)
  10. #
  11. # Copyright (C) 2017 Tom Kralidis (tomkralidis@gmail.com)
  12. #
  13. # This source is free software; you can redistribute it and/or modify it under
  14. # the terms of the GNU General Public License as published by the Free
  15. # Software Foundation; either version 2 of the License, or (at your option)
  16. # any later version.
  17. #
  18. # This code is distributed in the hope that it will be useful, but WITHOUT ANY
  19. # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  20. # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
  21. # details.
  22. #
  23. # You should have received a copy of the GNU General Public License along
  24. # with this program; if not, write to the Free Software Foundation, Inc.,
  25. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  26. #
  27. ###############################################################################
  28. import json
  29. import os.path
  30. from urllib.request import build_opener, install_opener, ProxyHandler
  31. from qgis.PyQt.QtCore import Qt
  32. from qgis.PyQt.QtWidgets import (QDialog, QComboBox,
  33. QDialogButtonBox, QMessageBox,
  34. QTreeWidgetItem, QWidget)
  35. from qgis.PyQt.QtGui import QColor
  36. from qgis.core import (Qgis, QgsApplication, QgsCoordinateReferenceSystem,
  37. QgsCoordinateTransform, QgsGeometry, QgsPointXY,
  38. QgsProviderRegistry, QgsSettings, QgsProject,
  39. QgsRectangle, QgsSettingsTree)
  40. from qgis.gui import QgsRubberBand, QgsGui
  41. from qgis.utils import OverrideCursor
  42. from MetaSearch import link_types
  43. from MetaSearch.dialogs.manageconnectionsdialog import ManageConnectionsDialog
  44. from MetaSearch.dialogs.newconnectiondialog import NewConnectionDialog
  45. from MetaSearch.dialogs.recorddialog import RecordDialog
  46. from MetaSearch.dialogs.apidialog import APIRequestResponseDialog
  47. from MetaSearch.search_backend import get_catalog_service
  48. from MetaSearch.util import (clean_ows_url, get_connections_from_file,
  49. get_ui_class, get_help_url, highlight_content,
  50. normalize_text, open_url, render_template,
  51. serialize_string, StaticContext)
  52. BASE_CLASS = get_ui_class('maindialog.ui')
  53. class MetaSearchDialog(QDialog, BASE_CLASS):
  54. """main dialogue"""
  55. def __init__(self, iface):
  56. """init window"""
  57. QDialog.__init__(self)
  58. self.setupUi(self)
  59. self.iface = iface
  60. self.map = iface.mapCanvas()
  61. self.settings = QgsSettings()
  62. self.catalog = None
  63. self.catalog_url = None
  64. self.catalog_username = None
  65. self.catalog_password = None
  66. self.catalog_type = None
  67. self.context = StaticContext()
  68. self.leKeywords.setShowSearchIcon(True)
  69. self.leKeywords.setPlaceholderText(self.tr('Search keywords'))
  70. self.setWindowTitle(self.tr('MetaSearch'))
  71. self.rubber_band = QgsRubberBand(self.map, Qgis.GeometryType.Polygon)
  72. self.rubber_band.setColor(QColor(255, 0, 0, 75))
  73. self.rubber_band.setWidth(5)
  74. # form inputs
  75. self.startfrom = 1
  76. self.constraints = []
  77. self.maxrecords = int(self.settings.value('/MetaSearch/returnRecords', 10))
  78. self.timeout = int(self.settings.value('/MetaSearch/timeout', 10))
  79. self.disable_ssl_verification = self.settings.value(
  80. '/MetaSearch/disableSSL', False, bool)
  81. # Services tab
  82. self.cmbConnectionsServices.activated.connect(self.save_connection)
  83. self.cmbConnectionsSearch.activated.connect(self.save_connection)
  84. self.btnServerInfo.clicked.connect(self.connection_info)
  85. self.btnAddDefault.clicked.connect(self.add_default_connections)
  86. self.btnRawAPIResponse.clicked.connect(self.show_api)
  87. self.tabWidget.currentChanged.connect(self.populate_connection_list)
  88. # server management buttons
  89. self.btnNew.clicked.connect(self.add_connection)
  90. self.btnEdit.clicked.connect(self.edit_connection)
  91. self.btnDelete.clicked.connect(self.delete_connection)
  92. self.btnLoad.clicked.connect(self.load_connections)
  93. self.btnSave.clicked.connect(save_connections)
  94. # Search tab
  95. self.treeRecords.itemSelectionChanged.connect(self.record_clicked)
  96. self.treeRecords.itemDoubleClicked.connect(self.show_metadata)
  97. self.btnSearch.clicked.connect(self.search)
  98. self.leKeywords.returnPressed.connect(self.search)
  99. # prevent dialog from closing upon pressing enter
  100. self.buttonBox.button(QDialogButtonBox.Close).setAutoDefault(False)
  101. # launch help from button
  102. self.buttonBox.helpRequested.connect(self.help)
  103. self.btnCanvasBbox.setAutoDefault(False)
  104. self.btnCanvasBbox.clicked.connect(self.set_bbox_from_map)
  105. self.btnGlobalBbox.clicked.connect(self.set_bbox_global)
  106. # navigation buttons
  107. self.btnFirst.clicked.connect(self.navigate)
  108. self.btnPrev.clicked.connect(self.navigate)
  109. self.btnNext.clicked.connect(self.navigate)
  110. self.btnLast.clicked.connect(self.navigate)
  111. self.mActionAddWms.triggered.connect(self.add_to_ows)
  112. self.mActionAddWfs.triggered.connect(self.add_to_ows)
  113. self.mActionAddWcs.triggered.connect(self.add_to_ows)
  114. self.mActionAddAms.triggered.connect(self.add_to_ows)
  115. self.mActionAddAfs.triggered.connect(self.add_to_ows)
  116. self.mActionAddGisFile.triggered.connect(self.add_gis_file)
  117. self.btnViewRawAPIResponse.clicked.connect(self.show_api)
  118. self.manageGui()
  119. def manageGui(self):
  120. """open window"""
  121. def _on_timeout_change(value):
  122. self.settings.setValue('/MetaSearch/timeout', value)
  123. self.timeout = value
  124. def _on_records_change(value):
  125. self.settings.setValue('/MetaSearch/returnRecords', value)
  126. self.maxrecords = value
  127. def _on_ssl_state_change(state):
  128. self.settings.setValue('/MetaSearch/disableSSL', bool(state))
  129. self.disable_ssl_verification = bool(state)
  130. self.tabWidget.setCurrentIndex(0)
  131. self.populate_connection_list()
  132. self.btnRawAPIResponse.setEnabled(False)
  133. # load settings
  134. self.spnRecords.setValue(self.maxrecords)
  135. self.spnRecords.valueChanged.connect(_on_records_change)
  136. self.spnTimeout.setValue(self.timeout)
  137. self.spnTimeout.valueChanged.connect(_on_timeout_change)
  138. self.disableSSLVerification.setChecked(self.disable_ssl_verification)
  139. self.disableSSLVerification.stateChanged.connect(_on_ssl_state_change)
  140. key = '/MetaSearch/%s' % self.cmbConnectionsSearch.currentText()
  141. self.catalog_url = self.settings.value('%s/url' % key)
  142. self.catalog_username = self.settings.value('%s/username' % key)
  143. self.catalog_password = self.settings.value('%s/password' % key)
  144. self.catalog_type = self.settings.value('%s/catalog-type' % key)
  145. self.set_bbox_global()
  146. self.reset_buttons()
  147. # install proxy handler if specified in QGIS settings
  148. self.install_proxy()
  149. # Services tab
  150. def populate_connection_list(self):
  151. """populate select box with connections"""
  152. self.settings.beginGroup('/MetaSearch/')
  153. self.cmbConnectionsServices.clear()
  154. self.cmbConnectionsServices.addItems(self.settings.childGroups())
  155. self.cmbConnectionsSearch.clear()
  156. self.cmbConnectionsSearch.addItems(self.settings.childGroups())
  157. self.settings.endGroup()
  158. self.set_connection_list_position()
  159. if self.cmbConnectionsServices.count() == 0:
  160. # no connections - disable various buttons
  161. state_disabled = False
  162. self.btnSave.setEnabled(state_disabled)
  163. # and start with connection tab open
  164. self.tabWidget.setCurrentIndex(1)
  165. # tell the user to add services
  166. msg = self.tr('No services/connections defined. To get '
  167. 'started with MetaSearch, create a new '
  168. 'connection by clicking \'New\' or click '
  169. '\'Add default services\'.')
  170. self.textMetadata.setHtml('<p><h3>%s</h3></p>' % msg)
  171. else:
  172. # connections - enable various buttons
  173. state_disabled = True
  174. self.btnServerInfo.setEnabled(state_disabled)
  175. self.btnEdit.setEnabled(state_disabled)
  176. self.btnDelete.setEnabled(state_disabled)
  177. def set_connection_list_position(self):
  178. """set the current index to the selected connection"""
  179. to_select = self.settings.value('/MetaSearch/selected')
  180. conn_count = self.cmbConnectionsServices.count()
  181. if conn_count == 0:
  182. self.btnDelete.setEnabled(False)
  183. self.btnServerInfo.setEnabled(False)
  184. self.btnEdit.setEnabled(False)
  185. # does to_select exist in cmbConnectionsServices?
  186. exists = False
  187. for i in range(conn_count):
  188. if self.cmbConnectionsServices.itemText(i) == to_select:
  189. self.cmbConnectionsServices.setCurrentIndex(i)
  190. self.cmbConnectionsSearch.setCurrentIndex(i)
  191. exists = True
  192. break
  193. # If we couldn't find the stored item, but there are some, default
  194. # to the last item (this makes some sense when deleting items as it
  195. # allows the user to repeatidly click on delete to remove a whole
  196. # lot of items)
  197. if not exists and conn_count > 0:
  198. # If to_select is null, then the selected connection wasn't found
  199. # by QgsSettings, which probably means that this is the first time
  200. # the user has used CSWClient, so default to the first in the list
  201. # of connetions. Otherwise default to the last.
  202. if not to_select:
  203. current_index = 0
  204. else:
  205. current_index = conn_count - 1
  206. self.cmbConnectionsServices.setCurrentIndex(current_index)
  207. self.cmbConnectionsSearch.setCurrentIndex(current_index)
  208. def save_connection(self):
  209. """save connection"""
  210. caller = self.sender().objectName()
  211. if caller == 'cmbConnectionsServices': # servers tab
  212. current_text = self.cmbConnectionsServices.currentText()
  213. elif caller == 'cmbConnectionsSearch': # search tab
  214. current_text = self.cmbConnectionsSearch.currentText()
  215. self.settings.setValue('/MetaSearch/selected', current_text)
  216. key = '/MetaSearch/%s' % current_text
  217. if caller == 'cmbConnectionsSearch': # bind to service in search tab
  218. self.catalog_url = self.settings.value('%s/url' % key)
  219. self.catalog_username = self.settings.value('%s/username' % key)
  220. self.catalog_password = self.settings.value('%s/password' % key)
  221. self.catalog_type = self.settings.value('%s/catalog-type' % key)
  222. if caller == 'cmbConnectionsServices': # clear server metadata
  223. self.textMetadata.clear()
  224. self.btnRawAPIResponse.setEnabled(False)
  225. def connection_info(self):
  226. """show connection info"""
  227. current_text = self.cmbConnectionsServices.currentText()
  228. key = '/MetaSearch/%s' % current_text
  229. self.catalog_url = self.settings.value('%s/url' % key)
  230. self.catalog_username = self.settings.value('%s/username' % key)
  231. self.catalog_password = self.settings.value('%s/password' % key)
  232. self.catalog_type = self.settings.value('%s/catalog-type' % key)
  233. # connect to the server
  234. if not self._get_catalog():
  235. return
  236. if self.catalog: # display service metadata
  237. self.btnRawAPIResponse.setEnabled(True)
  238. metadata = render_template('en', self.context,
  239. self.catalog.conn,
  240. self.catalog.service_info_template)
  241. style = QgsApplication.reportStyleSheet()
  242. self.textMetadata.clear()
  243. self.textMetadata.document().setDefaultStyleSheet(style)
  244. self.textMetadata.setHtml(metadata)
  245. # clear results and disable buttons in Search tab
  246. self.clear_results()
  247. def add_connection(self):
  248. """add new service"""
  249. conn_new = NewConnectionDialog()
  250. conn_new.setWindowTitle(self.tr('New Catalog Service'))
  251. if conn_new.exec_() == QDialog.Accepted: # add to service list
  252. self.populate_connection_list()
  253. self.textMetadata.clear()
  254. def edit_connection(self):
  255. """modify existing connection"""
  256. current_text = self.cmbConnectionsServices.currentText()
  257. url = self.settings.value('/MetaSearch/%s/url' % current_text)
  258. conn_edit = NewConnectionDialog(current_text)
  259. conn_edit.setWindowTitle(self.tr('Edit Catalog Service'))
  260. conn_edit.leName.setText(current_text)
  261. conn_edit.leURL.setText(url)
  262. conn_edit.leUsername.setText(
  263. self.settings.value('/MetaSearch/%s/username' % current_text))
  264. conn_edit.lePassword.setText(
  265. self.settings.value('/MetaSearch/%s/password' % current_text))
  266. conn_edit.cmbCatalogType.setCurrentText(
  267. self.settings.value('/MetaSearch/%s/catalog-type' % current_text))
  268. if conn_edit.exec_() == QDialog.Accepted: # update service list
  269. self.populate_connection_list()
  270. def delete_connection(self):
  271. """delete connection"""
  272. current_text = self.cmbConnectionsServices.currentText()
  273. key = '/MetaSearch/%s' % current_text
  274. msg = self.tr('Remove service {0}?').format(current_text)
  275. result = QMessageBox.question(
  276. self, self.tr('Delete Service'), msg,
  277. QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
  278. if result == QMessageBox.Yes: # remove service from list
  279. self.settings.remove(key)
  280. index_to_delete = self.cmbConnectionsServices.currentIndex()
  281. self.cmbConnectionsServices.removeItem(index_to_delete)
  282. self.cmbConnectionsSearch.removeItem(index_to_delete)
  283. self.set_connection_list_position()
  284. def load_connections(self):
  285. """load services from list"""
  286. ManageConnectionsDialog(1).exec_()
  287. self.populate_connection_list()
  288. def add_default_connections(self):
  289. """add default connections"""
  290. filename = os.path.join(self.context.ppath,
  291. 'resources', 'connections-default.xml')
  292. doc = get_connections_from_file(self, filename)
  293. if doc is None:
  294. return
  295. self.settings.beginGroup('/MetaSearch/')
  296. keys = self.settings.childGroups()
  297. self.settings.endGroup()
  298. for server in doc.findall('csw'):
  299. name = server.attrib.get('name')
  300. # check for duplicates
  301. if name in keys:
  302. msg = self.tr('{0} exists. Overwrite?').format(name)
  303. res = QMessageBox.warning(self,
  304. self.tr('Loading connections'), msg,
  305. QMessageBox.Yes | QMessageBox.No)
  306. if res != QMessageBox.Yes:
  307. continue
  308. # no dups detected or overwrite is allowed
  309. key = '/MetaSearch/%s' % name
  310. self.settings.setValue('%s/url' % key, server.attrib.get('url'))
  311. self.settings.setValue('%s/catalog-type' % key, server.attrib.get('catalog-type', 'OGC CSW 2.0.2'))
  312. self.populate_connection_list()
  313. # Settings tab
  314. def set_ows_save_title_ask(self):
  315. """save ows save strategy as save ows title, ask if duplicate"""
  316. self.settings.setValue('/MetaSearch/ows_save_strategy', 'title_ask')
  317. def set_ows_save_title_no_ask(self):
  318. """save ows save strategy as save ows title, do NOT ask if duplicate"""
  319. self.settings.setValue('/MetaSearch/ows_save_strategy', 'title_no_ask')
  320. def set_ows_save_temp_name(self):
  321. """save ows save strategy as save with a temporary name"""
  322. self.settings.setValue('/MetaSearch/ows_save_strategy', 'temp_name')
  323. # Search tab
  324. def set_bbox_from_map(self):
  325. """set bounding box from map extent"""
  326. crs = self.map.mapSettings().destinationCrs()
  327. try:
  328. crsid = int(crs.authid().split(':')[1])
  329. except IndexError: # no projection
  330. crsid = 4326
  331. extent = self.map.extent()
  332. if crsid != 4326: # reproject to EPSG:4326
  333. src = QgsCoordinateReferenceSystem(crsid)
  334. dest = QgsCoordinateReferenceSystem("EPSG:4326")
  335. xform = QgsCoordinateTransform(src, dest, QgsProject.instance())
  336. minxy = xform.transform(QgsPointXY(extent.xMinimum(),
  337. extent.yMinimum()))
  338. maxxy = xform.transform(QgsPointXY(extent.xMaximum(),
  339. extent.yMaximum()))
  340. minx, miny = minxy
  341. maxx, maxy = maxxy
  342. else: # 4326
  343. minx = extent.xMinimum()
  344. miny = extent.yMinimum()
  345. maxx = extent.xMaximum()
  346. maxy = extent.yMaximum()
  347. self.leNorth.setText(str(maxy)[0:9])
  348. self.leSouth.setText(str(miny)[0:9])
  349. self.leWest.setText(str(minx)[0:9])
  350. self.leEast.setText(str(maxx)[0:9])
  351. def set_bbox_global(self):
  352. """set global bounding box"""
  353. self.leNorth.setText('90')
  354. self.leSouth.setText('-90')
  355. self.leWest.setText('-180')
  356. self.leEast.setText('180')
  357. def search(self):
  358. """execute search"""
  359. self.catalog = None
  360. self.constraints = []
  361. # clear all fields and disable buttons
  362. self.clear_results()
  363. # set current catalog
  364. current_text = self.cmbConnectionsSearch.currentText()
  365. key = '/MetaSearch/%s' % current_text
  366. self.catalog_url = self.settings.value('%s/url' % key)
  367. self.catalog_username = self.settings.value('%s/username' % key)
  368. self.catalog_password = self.settings.value('%s/password' % key)
  369. self.catalog_type = self.settings.value('%s/catalog-type' % key)
  370. # start position and number of records to return
  371. self.startfrom = 1
  372. # bbox
  373. # CRS is WGS84 with axis order longitude, latitude
  374. # defined by 'urn:ogc:def:crs:OGC:1.3:CRS84'
  375. minx = self.leWest.text()
  376. miny = self.leSouth.text()
  377. maxx = self.leEast.text()
  378. maxy = self.leNorth.text()
  379. bbox = [minx, miny, maxx, maxy]
  380. keywords = self.leKeywords.text()
  381. # build request
  382. if not self._get_catalog():
  383. return
  384. # TODO: allow users to select resources types
  385. # to find ('service', 'dataset', etc.)
  386. try:
  387. with OverrideCursor(Qt.WaitCursor):
  388. self.catalog.query_records(bbox, keywords, self.maxrecords,
  389. self.startfrom)
  390. except Exception as err:
  391. QMessageBox.warning(self, self.tr('Search error'),
  392. self.tr('Search error: {0}').format(err))
  393. return
  394. if self.catalog.matches == 0:
  395. self.lblResults.setText(self.tr('0 results'))
  396. return
  397. self.display_results()
  398. def display_results(self):
  399. """display search results"""
  400. self.treeRecords.clear()
  401. position = self.catalog.returned + self.startfrom - 1
  402. msg = self.tr('Showing {0} - {1} of %n result(s)', 'number of results',
  403. self.catalog.matches).format(self.startfrom, position)
  404. self.lblResults.setText(msg)
  405. for rec in self.catalog.records():
  406. item = QTreeWidgetItem(self.treeRecords)
  407. if rec['type']:
  408. item.setText(0, normalize_text(rec['type']))
  409. else:
  410. item.setText(0, 'unknown')
  411. if rec['title']:
  412. item.setText(1, normalize_text(rec['title']))
  413. if rec['identifier']:
  414. set_item_data(item, 'identifier', rec['identifier'])
  415. self.btnViewRawAPIResponse.setEnabled(True)
  416. if self.catalog.matches < self.maxrecords:
  417. disabled = False
  418. else:
  419. disabled = True
  420. self.btnFirst.setEnabled(disabled)
  421. self.btnPrev.setEnabled(disabled)
  422. self.btnNext.setEnabled(disabled)
  423. self.btnLast.setEnabled(disabled)
  424. self.btnRawAPIResponse.setEnabled(False)
  425. def clear_results(self):
  426. """clear search results"""
  427. self.lblResults.clear()
  428. self.treeRecords.clear()
  429. self.reset_buttons()
  430. def record_clicked(self):
  431. """record clicked signal"""
  432. # disable only service buttons
  433. self.reset_buttons(True, False, False)
  434. self.rubber_band.reset()
  435. if not self.treeRecords.selectedItems():
  436. return
  437. item = self.treeRecords.currentItem()
  438. if not item:
  439. return
  440. identifier = get_item_data(item, 'identifier')
  441. try:
  442. record = next(item for item in self.catalog.records()
  443. if item['identifier'] == identifier)
  444. except KeyError:
  445. QMessageBox.warning(self,
  446. self.tr('Record parsing error'),
  447. 'Unable to locate record identifier')
  448. return
  449. # if the record has a bbox, show a footprint on the map
  450. if record['bbox'] is not None:
  451. bx = record['bbox']
  452. rt = QgsRectangle(float(bx['minx']), float(bx['miny']),
  453. float(bx['maxx']), float(bx['maxy']))
  454. geom = QgsGeometry.fromRect(rt)
  455. if geom is not None:
  456. src = QgsCoordinateReferenceSystem("EPSG:4326")
  457. dst = self.map.mapSettings().destinationCrs()
  458. if src.postgisSrid() != dst.postgisSrid():
  459. ctr = QgsCoordinateTransform(
  460. src, dst, QgsProject.instance())
  461. try:
  462. geom.transform(ctr)
  463. except Exception as err:
  464. QMessageBox.warning(
  465. self,
  466. self.tr('Coordinate Transformation Error'),
  467. str(err))
  468. self.rubber_band.setToGeometry(geom, None)
  469. # figure out if the data is interactive and can be operated on
  470. self.find_services(record, item)
  471. def find_services(self, record, item):
  472. """scan record for WMS/WMTS|WFS|WCS endpoints"""
  473. services = {}
  474. for link in record['links']:
  475. link = self.catalog.parse_link(link)
  476. if 'scheme' in link:
  477. link_type = link['scheme']
  478. elif 'protocol' in link:
  479. link_type = link['protocol']
  480. else:
  481. link_type = None
  482. if link_type is not None:
  483. link_type = link_type.upper()
  484. wmswmst_link_types = list(
  485. map(str.upper, link_types.WMSWMST_LINK_TYPES))
  486. wfs_link_types = list(map(str.upper, link_types.WFS_LINK_TYPES))
  487. wcs_link_types = list(map(str.upper, link_types.WCS_LINK_TYPES))
  488. ams_link_types = list(map(str.upper, link_types.AMS_LINK_TYPES))
  489. afs_link_types = list(map(str.upper, link_types.AFS_LINK_TYPES))
  490. gis_file_link_types = list(
  491. map(str.upper, link_types.GIS_FILE_LINK_TYPES))
  492. # if the link type exists, and it is one of the acceptable
  493. # interactive link types, then set
  494. all_link_types = (wmswmst_link_types + wfs_link_types +
  495. wcs_link_types + ams_link_types +
  496. afs_link_types + gis_file_link_types)
  497. if all([link_type is not None, link_type in all_link_types]):
  498. if link_type in wmswmst_link_types:
  499. services['wms'] = link['url']
  500. self.mActionAddWms.setEnabled(True)
  501. if link_type in wfs_link_types:
  502. services['wfs'] = link['url']
  503. self.mActionAddWfs.setEnabled(True)
  504. if link_type in wcs_link_types:
  505. services['wcs'] = link['url']
  506. self.mActionAddWcs.setEnabled(True)
  507. if link_type in ams_link_types:
  508. services['ams'] = link['url']
  509. self.mActionAddAms.setEnabled(True)
  510. if link_type in afs_link_types:
  511. services['afs'] = link['url']
  512. self.mActionAddAfs.setEnabled(True)
  513. if link_type in gis_file_link_types:
  514. services['gis_file'] = link['url']
  515. services['title'] = record.get('title', '')
  516. self.mActionAddGisFile.setEnabled(True)
  517. self.tbAddData.setEnabled(True)
  518. set_item_data(item, 'link', json.dumps(services))
  519. def navigate(self):
  520. """manage navigation / paging"""
  521. caller = self.sender().objectName()
  522. if caller == 'btnFirst':
  523. self.startfrom = 1
  524. elif caller == 'btnLast':
  525. self.startfrom = self.catalog.matches - self.maxrecords + 1
  526. elif caller == 'btnNext':
  527. if self.startfrom > self.catalog.matches - self.maxrecords:
  528. msg = self.tr('End of results. Go to start?')
  529. res = QMessageBox.information(self, self.tr('Navigation'),
  530. msg,
  531. (QMessageBox.Ok |
  532. QMessageBox.Cancel))
  533. if res == QMessageBox.Ok:
  534. self.startfrom = 1
  535. else:
  536. return
  537. else:
  538. self.startfrom += self.maxrecords
  539. elif caller == "btnPrev":
  540. if self.startfrom == 1:
  541. msg = self.tr('Start of results. Go to end?')
  542. res = QMessageBox.information(self, self.tr('Navigation'),
  543. msg,
  544. (QMessageBox.Ok |
  545. QMessageBox.Cancel))
  546. if res == QMessageBox.Ok:
  547. self.startfrom = (self.catalog.matches -
  548. self.maxrecords + 1)
  549. else:
  550. return
  551. elif self.startfrom <= self.maxrecords:
  552. self.startfrom = 1
  553. else:
  554. self.startfrom -= self.maxrecords
  555. # bbox
  556. # CRS is WGS84 with axis order longitude, latitude
  557. # defined by 'urn:ogc:def:crs:OGC:1.3:CRS84'
  558. minx = self.leWest.text()
  559. miny = self.leSouth.text()
  560. maxx = self.leEast.text()
  561. maxy = self.leNorth.text()
  562. bbox = [minx, miny, maxx, maxy]
  563. keywords = self.leKeywords.text()
  564. try:
  565. with OverrideCursor(Qt.WaitCursor):
  566. self.catalog.query_records(bbox, keywords,
  567. limit=self.maxrecords,
  568. offset=self.startfrom)
  569. except Exception as err:
  570. QMessageBox.warning(self, self.tr('Search error'),
  571. self.tr('Search error: {0}').format(err))
  572. return
  573. self.display_results()
  574. def add_to_ows(self):
  575. """add to OWS provider connection list"""
  576. conn_name_matches = []
  577. item = self.treeRecords.currentItem()
  578. if not item:
  579. return
  580. item_data = json.loads(get_item_data(item, 'link'))
  581. caller = self.sender().objectName()
  582. if caller == 'mActionAddWms':
  583. service_type = 'OGC:WMS/OGC:WMTS'
  584. sname = 'WMS'
  585. dyn_param = ['wms']
  586. provider_name = 'wms'
  587. setting_node = QgsSettingsTree.node('connections').childNode('ows').childNode('connections')
  588. data_url = item_data['wms']
  589. elif caller == 'mActionAddWfs':
  590. service_type = 'OGC:WFS'
  591. sname = 'WFS'
  592. dyn_param = ['wfs']
  593. provider_name = 'WFS'
  594. setting_node = QgsSettingsTree.node('connections').childNode('ows').childNode('connections')
  595. data_url = item_data['wfs']
  596. elif caller == 'mActionAddWcs':
  597. service_type = 'OGC:WCS'
  598. sname = 'WCS'
  599. dyn_param = ['wcs']
  600. provider_name = 'wcs'
  601. setting_node = QgsSettingsTree.node('connections').childNode('ows').childNode('connections')
  602. data_url = item_data['wcs']
  603. elif caller == 'mActionAddAfs':
  604. service_type = 'ESRI:ArcGIS:FeatureServer'
  605. sname = 'AFS'
  606. dyn_param = []
  607. provider_name = 'arcgisfeatureserver'
  608. setting_node = QgsSettingsTree.node('connections').childNode('arcgisfeatureserver')
  609. data_url = (item_data['afs'].split('FeatureServer')[0] + 'FeatureServer')
  610. keys = setting_node.items(dyn_param)
  611. sname = '%s from MetaSearch' % sname
  612. for key in keys:
  613. if key.startswith(sname):
  614. conn_name_matches.append(key)
  615. if conn_name_matches:
  616. sname = conn_name_matches[-1]
  617. # check for duplicates
  618. if sname in keys: # duplicate found
  619. msg = self.tr('Connection {0} exists. Overwrite?').format(sname)
  620. res = QMessageBox.warning(
  621. self, self.tr('Saving server'), msg,
  622. QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
  623. if res == QMessageBox.No: # assign new name with serial
  624. sname = serialize_string(sname)
  625. elif res == QMessageBox.Cancel:
  626. return
  627. # no dups detected or overwrite is allowed
  628. dyn_param.append(sname)
  629. setting_node.childSetting('url').setValue(clean_ows_url(data_url), dyn_param)
  630. # open provider window
  631. ows_provider = QgsGui.sourceSelectProviderRegistry().\
  632. createSelectionWidget(
  633. provider_name, self, Qt.Widget,
  634. QgsProviderRegistry.WidgetMode.Embedded)
  635. # connect dialog signals to iface slots
  636. if service_type == 'OGC:WMS/OGC:WMTS':
  637. ows_provider.addRasterLayer.connect(self.iface.addRasterLayer)
  638. conn_cmb = ows_provider.findChild(QWidget, 'cmbConnections')
  639. connect = 'btnConnect_clicked'
  640. elif service_type == 'OGC:WFS':
  641. def addVectorLayer(path, name):
  642. self.iface.addVectorLayer(path, name, 'WFS')
  643. ows_provider.addVectorLayer.connect(addVectorLayer)
  644. conn_cmb = ows_provider.findChild(QWidget, 'cmbConnections')
  645. connect = 'connectToServer'
  646. elif service_type == 'OGC:WCS':
  647. ows_provider.addRasterLayer.connect(self.iface.addRasterLayer)
  648. conn_cmb = ows_provider.findChild(QWidget, 'mConnectionsComboBox')
  649. connect = 'mConnectButton_clicked'
  650. elif service_type == 'ESRI:ArcGIS:FeatureServer':
  651. def addAfsLayer(path, name):
  652. self.iface.addVectorLayer(path, name, 'afs')
  653. ows_provider.addVectorLayer.connect(addAfsLayer)
  654. conn_cmb = ows_provider.findChild(QComboBox)
  655. connect = 'connectToServer'
  656. ows_provider.setModal(False)
  657. ows_provider.show()
  658. # open provider dialogue against added OWS
  659. index = conn_cmb.findText(sname)
  660. if index > -1:
  661. conn_cmb.setCurrentIndex(index)
  662. # only for wfs
  663. if service_type == 'OGC:WFS':
  664. ows_provider.cmbConnections_activated(index)
  665. elif service_type == 'ESRI:ArcGIS:FeatureServer':
  666. ows_provider.cmbConnections_activated(index)
  667. getattr(ows_provider, connect)()
  668. def add_gis_file(self):
  669. """add GIS file from result"""
  670. item = self.treeRecords.currentItem()
  671. if not item:
  672. return
  673. item_data = json.loads(get_item_data(item, 'link'))
  674. gis_file = item_data['gis_file']
  675. title = item_data['title']
  676. layer = self.iface.addVectorLayer(gis_file, title, "ogr")
  677. if not layer:
  678. self.iface.messageBar().pushWarning(None, "Layer failed to load!")
  679. def show_metadata(self):
  680. """show record metadata"""
  681. if not self.treeRecords.selectedItems():
  682. return
  683. item = self.treeRecords.currentItem()
  684. if not item:
  685. return
  686. identifier = get_item_data(item, 'identifier')
  687. auth = None
  688. if self.disable_ssl_verification:
  689. try:
  690. auth = Authentication(verify=False)
  691. except NameError:
  692. pass
  693. try:
  694. with OverrideCursor(Qt.WaitCursor):
  695. cat = get_catalog_service(self.catalog_url, # spellok
  696. catalog_type=self.catalog_type,
  697. timeout=self.timeout,
  698. username=self.catalog_username or None,
  699. password=self.catalog_password or None,
  700. auth=auth)
  701. record = cat.get_record(identifier)
  702. if cat.type == 'OGC API - Records':
  703. record['url'] = cat.conn.request
  704. elif cat.type == 'OGC CSW 2.0.2':
  705. record.url = cat.conn.request
  706. except Exception as err:
  707. QMessageBox.warning(
  708. self, self.tr('GetRecords error'),
  709. self.tr('Error getting response: {0}').format(err))
  710. return
  711. except KeyError as err:
  712. QMessageBox.warning(
  713. self, self.tr('Record parsing error'),
  714. self.tr('Unable to locate record identifier: {0}').format(err))
  715. return
  716. crd = RecordDialog()
  717. metadata = render_template('en', self.context,
  718. record, self.catalog.record_info_template)
  719. style = QgsApplication.reportStyleSheet()
  720. crd.textMetadata.document().setDefaultStyleSheet(style)
  721. crd.textMetadata.setHtml(metadata)
  722. crd.exec_()
  723. def show_api(self):
  724. """show API request / response"""
  725. crd = APIRequestResponseDialog()
  726. request_html = highlight_content(self.context, self.catalog.request,
  727. self.catalog.format)
  728. response_html = highlight_content(self.context, self.catalog.response,
  729. self.catalog.format)
  730. style = QgsApplication.reportStyleSheet()
  731. crd.txtbrAPIRequest.clear()
  732. crd.txtbrAPIResponse.clear()
  733. crd.txtbrAPIRequest.document().setDefaultStyleSheet(style)
  734. crd.txtbrAPIResponse.document().setDefaultStyleSheet(style)
  735. crd.txtbrAPIRequest.setHtml(request_html)
  736. crd.txtbrAPIResponse.setHtml(response_html)
  737. crd.exec_()
  738. def reset_buttons(self, services=True, api=True, navigation=True):
  739. """Convenience function to disable WMS/WMTS|WFS|WCS buttons"""
  740. if services:
  741. self.tbAddData.setEnabled(False)
  742. self.mActionAddWms.setEnabled(False)
  743. self.mActionAddWfs.setEnabled(False)
  744. self.mActionAddWcs.setEnabled(False)
  745. self.mActionAddAms.setEnabled(False)
  746. self.mActionAddAfs.setEnabled(False)
  747. self.mActionAddGisFile.setEnabled(False)
  748. if api:
  749. self.btnViewRawAPIResponse.setEnabled(False)
  750. if navigation:
  751. self.btnFirst.setEnabled(False)
  752. self.btnPrev.setEnabled(False)
  753. self.btnNext.setEnabled(False)
  754. self.btnLast.setEnabled(False)
  755. def help(self):
  756. """launch help"""
  757. open_url(get_help_url())
  758. def reject(self):
  759. """back out of dialogue"""
  760. QDialog.reject(self)
  761. self.rubber_band.reset()
  762. def _get_catalog(self):
  763. """convenience function to init catalog wrapper"""
  764. auth = None
  765. if self.disable_ssl_verification:
  766. try:
  767. auth = Authentication(verify=False)
  768. except NameError:
  769. pass
  770. # connect to the server
  771. with OverrideCursor(Qt.WaitCursor):
  772. try:
  773. self.catalog = get_catalog_service(
  774. self.catalog_url, catalog_type=self.catalog_type,
  775. timeout=self.timeout, username=self.catalog_username or None,
  776. password=self.catalog_password or None, auth=auth)
  777. return True
  778. except Exception as err:
  779. msg = self.tr('Error connecting to service: {0}').format(err)
  780. QMessageBox.warning(self, self.tr('CSW Connection error'), msg)
  781. return False
  782. def install_proxy(self):
  783. """set proxy if one is set in QGIS network settings"""
  784. # initially support HTTP for now
  785. if self.settings.value('/proxy/proxyEnabled') == 'true':
  786. if self.settings.value('/proxy/proxyType') == 'HttpProxy':
  787. ptype = 'http'
  788. else:
  789. return
  790. user = self.settings.value('/proxy/proxyUser')
  791. password = self.settings.value('/proxy/proxyPassword')
  792. host = self.settings.value('/proxy/proxyHost')
  793. port = self.settings.value('/proxy/proxyPort')
  794. proxy_up = ''
  795. proxy_port = ''
  796. if all([user != '', password != '']):
  797. proxy_up = f'{user}:{password}@'
  798. if port != '':
  799. proxy_port = ':%s' % port
  800. conn = f'{ptype}://{proxy_up}{host}{proxy_port}'
  801. install_opener(build_opener(ProxyHandler({ptype: conn})))
  802. def save_connections():
  803. """save servers to list"""
  804. ManageConnectionsDialog(0).exec_()
  805. def get_item_data(item, field):
  806. """return identifier for a QTreeWidgetItem"""
  807. return item.data(_get_field_value(field), 32)
  808. def set_item_data(item, field, value):
  809. """set identifier for a QTreeWidgetItem"""
  810. item.setData(_get_field_value(field), 32, value)
  811. def _get_field_value(field):
  812. """convenience function to return field value integer"""
  813. value = 0
  814. if field == 'identifier':
  815. value = 0
  816. if field == 'link':
  817. value = 1
  818. return value