diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index eb679fa..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "python.linting.pylintEnabled": true, - "python.linting.enabled": true, - "python.linting.pylintUseMinimalCheckers": false, -} \ No newline at end of file diff --git a/french_address.py b/french_address.py index 56c2013..a14a4d2 100644 --- a/french_address.py +++ b/french_address.py @@ -34,6 +34,7 @@ from .resources import * from .french_address_dockwidget import FrenchAddressDockWidget +from .french_address_widget_result_setting import FrenchAddressWidgetResultSetting class FrenchAddress: @@ -76,6 +77,7 @@ def __init__(self, iface): #print "** INITIALIZING FrenchAddress" self.pluginIsActive = False self.dockwidget = None + self.widgetResultSetting = None self.data_from_api = "" self.tool = None @@ -83,6 +85,7 @@ def __init__(self, iface): self.catch_tool_icon = QgsApplication.iconPath("cursors/mCapturePoint.svg") self.copy_icon = QgsApplication.iconPath("mActionEditCopy.svg") self.show_url_icon = QgsApplication.iconPath("mIconWms.svg") + self.result_setting_icon = QgsApplication.iconPath("mActionOptions.svg") self.clipboard = QApplication.clipboard() # noinspection PyMethodMayBeStatic @@ -190,7 +193,7 @@ def clear(self): def enable_disable_catch_tool(self): if not self.catch_tool_activate: - self.tool = CatchTool(self.iface, self.dockwidget, self) + self.tool = CatchTool(self.iface, self.dockwidget, self.widgetResultSetting, self) self.canvas.setMapTool(self.tool) self.canvas.setCursor(QCursor(QPixmap(self.catch_tool_icon))) self.catch_tool_activate = True @@ -226,7 +229,7 @@ def address_processing(self): response_coordinates = self.api_address.take_reverse_response_coordinates() response_properties.update(response_coordinates) self.api_address.populate_table_widget(response_properties) - point_wgs84 = self.api_address.take_search_response_label() + point_wgs84 = self.api_address.take_search_response_coordinates() self.coord.set_canvas_project(self.canvas) self.coord.set_destination_crs() self.coord.take_crs_from_project(self.iface) @@ -234,7 +237,7 @@ def address_processing(self): self.coord.set_latitude_longitude_crs(point_wgs84) self.coord.zoom_to_canvas(self.canvas) else: - message = self.tr(' Format of the address is not correct, please check log message.') + message = ' Le format de l\'adresse est incorrecte' self.iface.messageBar().pushMessage('Warning', message, level=Qgis.Warning, @@ -249,11 +252,11 @@ def set_visible_properties(self, state): def copy_to_clipboard(self): text_to_copy = self.dockwidget.le_input_address.toPlainText() - message = self.tr(' Nothing to copying') + message = ' Rien à copier' if text_to_copy != '': self.clipboard.setText(text_to_copy) - message = self.tr(f' {text_to_copy}, copied to the clipboard') + message = f' {text_to_copy}, copié dans le presse papier' self.iface.messageBar().pushMessage('Address', message, @@ -267,10 +270,10 @@ def open_map_url(self): id_house = self.data_from_api['features'][0]['properties']['id'] url = self.api_address.set_map_url(longitude_house, latitude_house, id_house) self.api_address.open_map_url(url) - text = self.tr(' Open in web browser ') + text = ' Ouvert dans le navigateur ' message = text + f'{url}' except: - message = self.tr(' Nothing to open in web browser') + message = ' Rien à ouvrir dans le navigateur' self.iface.messageBar().pushMessage('Address', message, @@ -293,6 +296,16 @@ def set_connections(self): self.dockwidget.tb_open_url.clicked.connect( self.open_map_url ) + self.dockwidget.tb_result_setting.clicked.connect( + self.show_result_setting + ) + + def show_result_setting(self): + + if self.widgetResultSetting is None: + self.widgetResultSetting = FrenchAddressWidgetResultSetting() + + self.widgetResultSetting.show() def run(self): """Run method that loads and starts the plugin""" @@ -302,6 +315,7 @@ def run(self): if self.dockwidget is None: self.dockwidget = FrenchAddressDockWidget() + self.widgetResultSetting = FrenchAddressWidgetResultSetting() self.dockwidget.tw_details.setVisible(False) self.coord = Coordinates(self.dockwidget) self.address = Address(self.dockwidget) @@ -309,6 +323,7 @@ def run(self): self.dockwidget.tb_catch_tool.setIcon(QIcon(self.catch_tool_icon)) self.dockwidget.pb_copy.setIcon(QIcon(self.copy_icon)) self.dockwidget.tb_open_url.setIcon(QIcon(self.show_url_icon)) + self.dockwidget.tb_result_setting.setIcon(QIcon(self.result_setting_icon)) self.set_connections() self.dockwidget.closingPlugin.connect(self.onClosePlugin) diff --git a/french_address_dockwidget_base.ui b/french_address_dockwidget_base.ui index 7f9f8b9..edfcf90 100644 --- a/french_address_dockwidget_base.ui +++ b/french_address_dockwidget_base.ui @@ -7,7 +7,7 @@ 0 0 422 - 368 + 373 @@ -18,45 +18,20 @@ QLayout::SetMinAndMaxSize - - - - - 0 - 0 - - - - - 16777215 - 48 - - - - false - - - Qt::TextEditorInteraction - - - Your address here, like 8 Boulevard du Port 80000 + + + + + 8 + true + - - - - - Research - - - true - - - false + Résultat : - + @@ -71,7 +46,7 @@ - Click on the map to capture an address + Cliquer sur la carte pour capturer l'adresse ... @@ -90,20 +65,7 @@ - - - - - MS Shell Dlg 2 - 8 - - - - Show more details - - - - + @@ -113,7 +75,6 @@ - MS Shell Dlg 2 8 @@ -137,17 +98,42 @@ - attribute + attributs - value + valeurs - + + + + + 0 + 0 + + + + + 16777215 + 48 + + + + false + + + Qt::TextEditorInteraction + + + Votre adresse ici, comme 8 Boulevard du Port 80000 + + + + @@ -162,28 +148,39 @@ - Copy result to clipboard + Copier dans le presse papier ... - - + + + + Rechercher + + + true + + + false + + + + + 8 - 75 - true - Result: + Montrer plus de détails - + @@ -198,7 +195,29 @@ - Show address on website 'adresse.data.gouv.fr' + Montre l'adresse sur le site 'adresse.data.gouv.fr' + + + ... + + + + + + + + 24 + 24 + + + + + 24 + 24 + + + + Paramètre résultat ... diff --git a/french_address_result_setting.ui b/french_address_result_setting.ui new file mode 100644 index 0000000..2198f6e --- /dev/null +++ b/french_address_result_setting.ui @@ -0,0 +1,105 @@ + + + Form + + + + 0 + 0 + 266 + 158 + + + + + 0 + 0 + + + + + 266 + 158 + + + + + 266 + 158 + + + + Paramètre résultat + + + + + 9 + 51 + 151 + 20 + + + + postcode (code postale) + + + + + + 9 + 77 + 141 + 20 + + + + citycode (code INSEE) + + + + + + 9 + 103 + 33 + 20 + + + + id + + + + + + 9 + 129 + 63 + 20 + + + + context + + + + + + 9 + 9 + 233 + 32 + + + + Activé les éléments pour les faires apparaître dans la partie résultat. + + + true + + + + + + diff --git a/french_address_widget_result_setting.py b/french_address_widget_result_setting.py new file mode 100644 index 0000000..7ea80d1 --- /dev/null +++ b/french_address_widget_result_setting.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + FrenchAddressDockWidget + A QGIS plugin + Recherche et localisation d'adresse française. + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2020-04-20 + git sha : $Format:%H$ + copyright : (C) 2020 by Guillaume DELPLANQUE + email : delpro.guillaume@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" + +import os + +from qgis.PyQt import QtGui, QtWidgets, uic +from qgis.PyQt.QtCore import pyqtSignal + +FORM_CLASS, _ = uic.loadUiType(os.path.join( + os.path.dirname(__file__), 'french_address_result_setting.ui')) + + +class FrenchAddressWidgetResultSetting(QtWidgets.QDockWidget, FORM_CLASS): + + closingPlugin = pyqtSignal() + + def __init__(self, parent=None): + """Constructor.""" + super(FrenchAddressWidgetResultSetting, self).__init__(parent) + # Set up the user interface from Designer. + # After setupUI you can access any designer object by doing + # self., and you can use autoconnect slots - see + # http://doc.qt.io/qt-5/designer-using-a-ui-file.html + # #widgets-and-dialogs-with-auto-connect + self.setupUi(self) + + def closeEvent(self, event): + self.closingPlugin.emit() + event.accept() diff --git a/i18n/FrenchAddress_fr.qm b/i18n/FrenchAddress_fr.qm deleted file mode 100644 index 3678efc..0000000 Binary files a/i18n/FrenchAddress_fr.qm and /dev/null differ diff --git a/i18n/FrenchAddress_fr.ts b/i18n/FrenchAddress_fr.ts deleted file mode 100644 index 62445a1..0000000 --- a/i18n/FrenchAddress_fr.ts +++ /dev/null @@ -1,298 +0,0 @@ - - - - - Address - - - The address format is not respected - - example: 20 Avenue de Ségur 75007 Paris - Le format de l'adresse n'est pas respecté - - exemple: 20 Avenue de Ségur 75007 Paris - - - - You need a house number - - example: 20 Avenue de Ségur 75007 - Vous avez besoin d'un numéro de maison - - exemple: 20 Avenue de Ségur 75007 - - - - Missing a street name - - example: 20 Avenue de Ségur 75007 - Il manque un nom de rue - - exemple: 20 Avenue de Ségur 75007 - - - - The postal code is missing - - example: 20 Avenue de Ségur 75007 - Le code postal est manquant - - exemple: 20 Avenue de Ségur 75007 - - - - The address format is not respected - - example: 20 Avenue de Ségur 75007 Paris - Le format de l'adresse n'est pas respecté - - exemple: 20 Avenue de Ségur 75007 Paris - - - - You need a house number - - example: 20 Avenue de Ségur 75007 - Vous avez besoin d'un numéro de maison - - exemple: 20 Avenue de Ségur 75007 - - - - Missing a street name - - example: 20 Avenue de Ségur 75007 - Il manque un nom de rue - - exemple: 20 Avenue de Ségur 75007 - - - - The postal code is missing - - example: 20 Avenue de Ségur 75007 - Le code postal est manquant - - exemple: 20 Avenue de Ségur 75007 - - - - The address format is not respected - - example: 20 Avenue de Ségur 75007 Paris - Le format de l'adresse n'est pas respecté - - exemple: 20 Avenue de Ségur 75007 Paris - - - - You need a house number - - example: 20 Avenue de Ségur 75007 - Vous avez besoin d'un numéro de maison - - exemple: 20 Avenue de Ségur 75007 - - - - Missing a street name - - example: 20 Avenue de Ségur 75007 - Il manque un nom de rue - - exemple: 20 Avenue de Ségur 75007 - - - - The postal code is missing - - example: 20 Avenue de Ségur 75007 - Le code postal est manquant - - exemple: 20 Avenue de Ségur 75007 - - - - The address format is not respected example: 20 Avenue de Ségur 75007 Paris - Le format de l'adresse n'est pas respecté exemple: 20 Avenue de Ségur 75007 Paris - - - - You need a house number example: 20 Avenue de Ségur 75007 - Vous avez besoin d'un numéro de maison exemple: 20 Avenue de Ségur 75007 - - - - Missing a street name example: 20 Avenue de Ségur 75007 - Il manque un nom de rue exemple: 20 Avenue de Ségur 75007 - - - - The postal code is missing example: 20 Avenue de Ségur 75007 - Le code postal est manquant exemple: 20 Avenue de Ségur 75007 - - - - Address is complete - L'adresse est complète - - - - The address format is not respected example: 20 Avenue de Ségur 75007 Paris - Le format de l'adresse n'est pas respecté exemple: 20 Avenue de Ségur 75007 Paris - - - - You need a house number example: 20 Avenue de Ségur 75007 - Vous avez besoin d'un numéro de maison exemple: 20 Avenue de Ségur 75007 - - - - Missing a street name example: 20 Avenue de Ségur 75007 - Il manque un nom de rue exemple: 20 Avenue de Ségur 75007 - - - - The postal code is missing example: 20 Avenue de Ségur 75007 - Le code postal est manquant exemple: 20 Avenue de Ségur 75007 - - - - ApiAddress - - - No address found at this coordinates - Aucune adresse trouvée à ces coordonnées - - - - There is no address with this entry - Il n'y a pas d'adresse avec c'est valeurs - - - - The connection failed - La connexion a échoué - - - - Connection established - Connection établie - - - - attribute - attribut - - - - value - valeur - - - - CatchTool - - - no address found at this coordinates - aucune adresse trouvée à ces coordonnées - - - - click on the map to capture an address... - cliquer sur la carte pour capturer une adresse... - - - - FrenchAddress - - - Format of the address is not correct, please check log message. - Le format de l'adresse n'est pas correct, veuillez vérifier le message du journal. - - - - Nothing to copying - Rien à copier - - - - Nothing to open in web browser - Rien à ouvrir dans le navigateur Web - - - - Open in web browser - Ouvertr dans le navigateur Web - - - - FrenchAddressDockWidgetBase - - - French Address - French Address - - - - Your address here, like 8 Boulevard du Port 80000 - Votre adresse ici, exemple 8 Boulevard du Port 80000 - - - - Search - Chercher - - - - Click on the map to capture an address - Cliquez sur la carte pour capturer une adresse - - - - ... - ... - - - - Show more details - Afficher plus de détails - - - - attribute - attribut - - - - value - valeur - - - - Copy result to clipboard - Copier le résultat dans le presse-papiers - - - - ... - Copy to clipboard - ... - - - - Result: - Résultat: - - - - Show address on website 'adresse.data.gouv.fr' - Afficher l'adresse sur le site 'adresse.data.gouv.fr' - - - - Research - Rechercher - - - diff --git a/metadata.txt b/metadata.txt index c7dfec4..7e5201b 100644 --- a/metadata.txt +++ b/metadata.txt @@ -7,18 +7,18 @@ name=French Address qgisMinimumVersion=3.4 description=Research and location based on the API BAN provided by the French government . description[fr]=Recherche et localisation basées sur l'API BAN fournie par le gouvernement français. -version=1.2 +version=1.3.0 author=Guillaume DELPLANQUE email=delpro.guillaume@gmail.com about=For research an address, simply write the desired address in the plugin's research bar. For best results, enter a house number, street name, and zip code. You can also retrieve the address based on the coordinates by activating the tool "Click on the map to capture an address". - The cursor will change to a cross and you can start typing. + The cursor will change to a cross and you can start catching. about[fr]=Pour rechercher une adresse, il suffit d'écrire l'adresse souhaitée dans la barre de recherche du plugin. Pour de meilleurs résultats, entrez un numéro de maison, un nom de rue et le code postal. - Vous pouvez également récupérez l'adresse en fonction des coordonnées en activant l'outil' "Cliquez sur la carte pour capturer une adresse". - Le curseur se changera en croix et vous pourrez commencer la saisie. + Vous pouvez également récupérez l'adresse en fonction des coordonnées en activant l'outil' "Cliquez sur la carte pour capturer l'adresse". + Le curseur se changera en croix et vous pourrez commencer la capture. tracker=https://github.com/Gui0delp/french_address/issues repository=https://github.com/Gui0delp/french_address @@ -28,10 +28,15 @@ repository=https://github.com/Gui0delp/french_address hasProcessingProvider=no # Uncomment the following line and add your changelog: -changelog=1.2 +changelog=1.3.0 + - added setting panel for choose result format + 1.2.0 + - refact api address module + - add network access manager by Alessandro Pasotti + 1.1.1 - fix bug when deactivate the catch tool - updated translate file for French language - 1.1 + 1.1.0 - now you can view more details from result - redesign of the capture tool - add tool to show address on the web browser diff --git a/modules/address.py b/modules/address.py index 8b1f42a..76a3629 100644 --- a/modules/address.py +++ b/modules/address.py @@ -29,7 +29,7 @@ def test_address_entry(self, entry): self.entry = re.match(self.pattern_address, str(entry)) flag = True else: - self.message_log(self.tr('The address format is not respected example: 20 Avenue de Ségur 75007 Paris')) + self.message_log("Le format de l'adresse n'ai pas respecté, exemple: 20 Avenue de Ségur 75007 Paris") flag = False return flag @@ -45,18 +45,18 @@ def test_obligatory_field(self): test = True if not self.house_number: test = False - self.message_log(self.tr('You need a house number example: 20 Avenue de Ségur 75007')) + self.message_log("Vous devez avoir un numéro de rue, exemple: 20 Avenue de Ségur 75007") if not self.name_road: test = False - self.message_log(self.tr('Missing a street name example: 20 Avenue de Ségur 75007')) + self.message_log("Il manque un nom de rue, exemple: 20 Avenue de Ségur 75007") if not self.postcode: test = False - self.message_log(self.tr('The postal code is missing example: 20 Avenue de Ségur 75007')) + self.message_log("Le code postal est manquant, exemple: 20 Avenue de Ségur 75007") if test: - self.message_log(self.tr('Address is complete')) + self.message_log("Adresse complète") return test def message_log(self, msg=""): diff --git a/modules/api_address.py b/modules/api_address.py index 99900b5..b33239a 100644 --- a/modules/api_address.py +++ b/modules/api_address.py @@ -10,6 +10,8 @@ from PyQt5.Qt import QApplication, QUrl, QDesktopServices from qgis.PyQt.QtCore import QTranslator, QCoreApplication +from .networkaccessmanager import NetworkAccessManager, RequestsException + class ApiAddress: """ This class manage the methods for api @@ -18,13 +20,15 @@ class ApiAddress: def __init__(self, dialog=None): self.dialog = dialog self.zoom_to_map = 19 - self.reverse_url = "https://api-adresse.data.gouv.fr/reverse/" - self.search_url = "https://api-adresse.data.gouv.fr/search/" - self.map_url = "https://adresse.data.gouv.fr/base-adresse-nationale/" + self.REVERSE_URL = "https://api-adresse.data.gouv.fr/reverse/" + self.SEARCH_URL = "https://api-adresse.data.gouv.fr/search/" + self.MAP_URL = "https://adresse.data.gouv.fr/base-adresse-nationale/" + self.USER_AGENT = b'Mozilla/5.0 QGIS LocatorFilter' + self.headers = {b'User-Agent': self.USER_AGENT} self.url = "" - self.response = "" + self.content = "" self.json_data = "" - self.search_label = "" + self.search_coordinates = "" self.my_context = ssl._create_unverified_context() self.latitude = "" self.longitude = "" @@ -33,10 +37,12 @@ def __init__(self, dialog=None): self.reverse_properties = {} self.reverse_coordinates = {} - self.error_message_no_address_locate = self.tr("No address found at this coordinates") - self.error_message_no_address_found = self.tr("There is no address with this entry") - self.error_message_connection = self.tr('The connection failed') - self.success_message_connection = self.tr('Connection established') + self.nam = NetworkAccessManager() + + self.error_message_no_address_locate = "Pas d'adresse trouvé avec ces coordonnées" + self.error_message_no_address_found = "Pas d'adresse avec ces critères" + self.error_message_connection = "La connexion a échouée" + self.success_message_connection = "Connexion établie" def tr(self, message): return QCoreApplication.translate('FrenchAddress', message) @@ -45,7 +51,7 @@ def set_reverse_url(self, longitude, latitude): """Set the reverse url with the longitude and latitude""" lon = str(longitude) lat = str(latitude) - self.url = self.reverse_url + '?lon=' + lon + '&lat=' + lat + self.url = self.REVERSE_URL + '?lon=' + lon + '&lat=' + lat return self.url def set_map_url(self, longitude_house, latitude_house, id_house): @@ -53,7 +59,7 @@ def set_map_url(self, longitude_house, latitude_house, id_house): lon = str(longitude_house) lat = str(latitude_house) id = id_house - url_for_map = self.map_url + str(id) + '#' + str(self.zoom_to_map) + '/' + lat + '/' + lon + url_for_map = self.MAP_URL + str(id) + '#' + str(self.zoom_to_map) + '/' + lat + '/' + lon return url_for_map def open_map_url(self, url_for_map): @@ -63,7 +69,9 @@ def open_map_url(self, url_for_map): def test_request(self): """test if the request is OK""" try: - urlopen(self.url, context=self.my_context) + headers = {b'User-Agent': self.USER_AGENT} + (response, content) = self.nam.request(self.url, headers=headers, blocking=True) + #urlopen(self.url, context=self.my_context) self.message_log(f'{self.success_message_connection}: {self.url}') return True except: @@ -72,12 +80,14 @@ def test_request(self): def set_request(self): """Open the url""" - self.response = urlopen(self.url, context=self.my_context) - return self.response + headers = {b'User-Agent': self.USER_AGENT} + (response, content) = self.nam.request(self.url, headers=headers, blocking=True) + self.content = content + return self.content def decode_response(self): """decode with the utf-8 encodage, the response""" - self.json_data = self.response.read().decode('utf-8') + self.json_data = self.content.decode('utf-8') return self.json_data def json_to_dictionnary(self): @@ -120,7 +130,7 @@ def set_search_url(self, house_number, name_road, post_code): name_parse = urllib.parse.quote(name_road, safe='\'') - self.url = self.search_url \ + self.url = self.SEARCH_URL \ + '?q=' + house_number \ + '%20' + name_parse \ + post_code \ @@ -128,22 +138,21 @@ def set_search_url(self, house_number, name_road, post_code): + '&autocomplete=1' return self.url - def take_search_response_label(self): - """Return the label of the request""" + def take_search_response_coordinates(self): + """Return the coordinates of the request""" try: - self.search_label = \ - self.dictionnary_data['features'][0]['geometry']['coordinates'] + self.search_coordinates = self.dictionnary_data['features'][0]['geometry']['coordinates'] except: self.message_log(self.error_message_no_address_found) - return self.search_label + return self.search_coordinates def initialize_table_widget(self): self.dialog.tw_details.clear() self.dialog.tw_details.setRowCount(0) self.dialog.tw_details.setColumnCount(2) - head_attribute = self.tr("attribute") - head_value = self.tr("value") + head_attribute = "attributs" + head_value = "valeurs" self.dialog.tw_details.setHorizontalHeaderItem(0, QTableWidgetItem(head_attribute)) self.dialog.tw_details.setHorizontalHeaderItem(1, QTableWidgetItem(head_value)) diff --git a/modules/catch_tool.py b/modules/catch_tool.py index 33299b4..0c1ebf2 100644 --- a/modules/catch_tool.py +++ b/modules/catch_tool.py @@ -1,24 +1,21 @@ """Manage the tool""" from qgis.gui import QgsMapTool from qgis.core import Qgis, QgsMessageLog -from qgis.PyQt.QtCore import QTranslator, QCoreApplication from .coordinates import Coordinates from .api_address import ApiAddress class CatchTool(QgsMapTool): - def __init__(self, iface, dialog, fr_address_instance): + def __init__(self, iface, dialog, setting_widget, fr_address_instance): QgsMapTool.__init__(self, iface.mapCanvas()) self.canvas = iface.mapCanvas() self.iface = iface self.dialog = dialog + self.setting_widget = setting_widget self.fr_address_instance = fr_address_instance self.coord = Coordinates(self.dialog) self.api_address = ApiAddress(self.dialog) - def tr(self, message): - return QCoreApplication.translate('FrenchAddress', message) - def canvasReleaseEvent(self, event): x = event.pos().x() y = event.pos().y() @@ -41,10 +38,11 @@ def canvasReleaseEvent(self, event): response_properties = self.api_address.take_reverse_response_properties() response_coordinates = self.api_address.take_reverse_response_coordinates() response_properties.update(response_coordinates) - self.dialog.le_input_address.setText(response_label) + response = self.choose_results(response_label, response_properties) + self.dialog.le_input_address.setText(response) self.api_address.populate_table_widget(response_properties) else: - message = self.tr(' no address found at this coordinates ') + message = " pas d'adresse trouvé avec ces coordonnées" message_error = message + f'EPSG:4326 lon,lat = {self.coord.longitude},{self.coord.latitude}' self.message_log(message_error) self.iface.messageBar().pushMessage('Warning', @@ -53,7 +51,7 @@ def canvasReleaseEvent(self, event): ) def activate(self): - message = self.tr(' click on the map to capture an address...') + message = " cliquer sur la carte pour capturer l'adresse..." self.iface.messageBar().pushMessage('Info', message, level=Qgis.Info, @@ -68,5 +66,22 @@ def deactivate(self): self.fr_address_instance.catch_tool_activate = False self.deactivated.emit() + def choose_results(self, response_label, response_properties): + response = response_label + + if self.setting_widget.cbox_postcode.isChecked(): + response = response + ' ' + response_properties['postcode'] + + if self.setting_widget.cbox_citycode.isChecked(): + response = response + ' ' + response_properties['citycode'] + + if self.setting_widget.cbox_id.isChecked(): + response = response + ' ' + response_properties['id'] + + if self.setting_widget.cbox_type.isChecked(): + response = response + ' ' + response_properties['context'] + + return response + def message_log(self, msg=""): QgsMessageLog.logMessage('{} {}'.format(self.__class__.__name__, msg), 'FrenchAddress', Qgis.Info) diff --git a/modules/networkaccessmanager.py b/modules/networkaccessmanager.py new file mode 100644 index 0000000..a8426a4 --- /dev/null +++ b/modules/networkaccessmanager.py @@ -0,0 +1,385 @@ +# -*- coding: utf-8 -*- +""" +*************************************************************************** + An httplib2 replacement that uses QgsNetworkAccessManager + + https://github.com/boundlessgeo/lib-qgis-commons/blob/master/qgiscommons2/network/networkaccessmanager.py + + --------------------- + Date : August 2016 + Copyright : (C) 2016 Boundless, http://boundlessgeo.com + Email : apasotti at boundlessgeo dot com +*************************************************************************** +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +* * +*************************************************************************** +""" +from future import standard_library +standard_library.install_aliases() +from builtins import str +from builtins import object + +__author__ = 'Alessandro Pasotti' +__date__ = 'August 2016' + +import re +import urllib.request, urllib.error, urllib.parse + +from qgis.PyQt.QtCore import pyqtSlot, QUrl, QEventLoop, QTimer, QCoreApplication, QObject +from qgis.PyQt.QtNetwork import QNetworkRequest, QNetworkReply + +from qgis.core import QgsNetworkAccessManager, QgsAuthManager, QgsMessageLog + +# FIXME: ignored +DEFAULT_MAX_REDIRECTS = 4 + +class RequestsException(Exception): + pass + +class RequestsExceptionTimeout(RequestsException): + pass + +class RequestsExceptionConnectionError(RequestsException): + pass + +class RequestsExceptionUserAbort(RequestsException): + pass + +class Map(dict): + """ + Example: + m = Map({'first_name': 'Eduardo'}, last_name='Pool', age=24, sports=['Soccer']) + """ + def __init__(self, *args, **kwargs): + super(Map, self).__init__(*args, **kwargs) + for arg in args: + if isinstance(arg, dict): + for k, v in arg.items(): + self[k] = v + + if kwargs: + for k, v in kwargs.items(): + self[k] = v + + def __getattr__(self, attr): + return self.get(attr) + + def __setattr__(self, key, value): + self.__setitem__(key, value) + + def __setitem__(self, key, value): + super(Map, self).__setitem__(key, value) + self.__dict__.update({key: value}) + + def __delattr__(self, item): + self.__delitem__(item) + + def __delitem__(self, key): + super(Map, self).__delitem__(key) + del self.__dict__[key] + + +class Response(Map): + pass + +class NetworkAccessManager(object): + """ + This class mimicks httplib2 by using QgsNetworkAccessManager for all + network calls. + The return value is a tuple of (response, content), the first being and + instance of the Response class, the second being a string that contains + the response entity body. + Parameters + ---------- + debug : bool + verbose logging if True + exception_class : Exception + Custom exception class + Usage 1 (blocking mode) + ----- + :: + nam = NetworkAccessManager(authcgf) + try: + (response, content) = nam.request('http://www.example.com') + except RequestsException as e: + # Handle exception + pass + Usage 2 (Non blocking mode) + ------------------------- + :: + NOTE! if blocking mode returns immediatly + it's up to the caller to manage listeners in case + of non blocking mode + nam = NetworkAccessManager(authcgf) + try: + nam.request('http://www.example.com', blocking=False) + nam.reply.finished.connect(a_signal_listener) + except RequestsException as e: + # Handle exception + pass + Get response using method: + nam.httpResult() that return a dictionary with keys: + 'status' - http code result come from reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) + 'status_code' - http code result come from reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) + 'status_message' - reply message string from reply.attribute(QNetworkRequest.HttpReasonPhraseAttribute) + 'content' - bytearray returned from reply + 'ok' - request success [True, False] + 'headers' - Dicionary containing the reply header + 'reason' - fomatted message string with reply.errorString() + 'exception' - the exception returne dduring execution + """ + + def __init__(self, authid=None, disable_ssl_certificate_validation=False, exception_class=None, debug=False): + self.disable_ssl_certificate_validation = disable_ssl_certificate_validation + self.authid = authid + self.reply = None + self.debug = debug + self.exception_class = exception_class + self.on_abort = False + self.blocking_mode = False + self.http_call_result = Response({ + 'status': 0, + 'status_code': 0, + 'status_message': '', + 'content': '', + 'ok': False, + 'headers': {}, + 'reason': '', + 'exception': None, + }) + + def msg_log(self, msg): + if self.debug: + QgsMessageLog.logMessage(msg, "NetworkAccessManager") + + def httpResult(self): + return self.http_call_result + + def request(self, url, method="GET", body=None, headers=None, redirections=DEFAULT_MAX_REDIRECTS, + connection_type=None, blocking=True): + """ + Make a network request by calling QgsNetworkAccessManager. + redirections argument is ignored and is here only for httplib2 compatibility. + """ + self.msg_log(u'http_call request: {0}'.format(url)) + + self.blocking_mode = blocking + req = QNetworkRequest() + # Avoid double quoting form QUrl + url = urllib.parse.unquote(url) + req.setUrl(QUrl(url)) + if headers is not None: + # This fixes a wierd error with compressed content not being correctly + # inflated. + # If you set the header on the QNetworkRequest you are basically telling + # QNetworkAccessManager "I know what I'm doing, please don't do any content + # encoding processing". + # See: https://bugs.webkit.org/show_bug.cgi?id=63696#c1 + try: + del headers['Accept-Encoding'] + except KeyError: + pass + for k, v in list(headers.items()): + self.msg_log("Setting header %s to %s" % (k, v)) + req.setRawHeader(k, v) + if self.authid: + self.msg_log("Update request w/ authid: {0}".format(self.authid)) + QgsAuthManager.instance().updateNetworkRequest(req, self.authid) + if self.reply is not None and self.reply.isRunning(): + self.reply.close() + if method.lower() == 'delete': + func = getattr(QgsNetworkAccessManager.instance(), 'deleteResource') + else: + func = getattr(QgsNetworkAccessManager.instance(), method.lower()) + # Calling the server ... + # Let's log the whole call for debugging purposes: + self.msg_log("Sending %s request to %s" % (method.upper(), req.url().toString())) + self.on_abort = False + headers = {str(h): str(req.rawHeader(h)) for h in req.rawHeaderList()} + for k, v in list(headers.items()): + self.msg_log("%s: %s" % (k, v)) + if method.lower() in ['post', 'put']: + if isinstance(body, file): + body = body.read() + self.reply = func(req, body) + else: + self.reply = func(req) + if self.authid: + self.msg_log("Update reply w/ authid: {0}".format(self.authid)) + QgsAuthManager.instance().updateNetworkReply(self.reply, self.authid) + + # necessary to trap local timout manage by QgsNetworkAccessManager + # calling QgsNetworkAccessManager::abortRequest + QgsNetworkAccessManager.instance().requestTimedOut.connect(self.requestTimedOut) + + self.reply.sslErrors.connect(self.sslErrors) + self.reply.finished.connect(self.replyFinished) + self.reply.downloadProgress.connect(self.downloadProgress) + + # block if blocking mode otherwise return immediatly + # it's up to the caller to manage listeners in case of no blocking mode + if not self.blocking_mode: + return (None, None) + + # Call and block + self.el = QEventLoop() + self.reply.finished.connect(self.el.quit) + + # Catch all exceptions (and clean up requests) + try: + self.el.exec_(QEventLoop.ExcludeUserInputEvents) + except Exception as e: + raise e + + if self.reply: + self.reply.finished.disconnect(self.el.quit) + + # emit exception in case of error + if not self.http_call_result.ok: + if self.http_call_result.exception and not self.exception_class: + raise self.http_call_result.exception + else: + raise self.exception_class(self.http_call_result.reason) + + return (self.http_call_result, self.http_call_result.content) + + #@pyqtSlot() + def downloadProgress(self, bytesReceived, bytesTotal): + """Keep track of the download progress""" + #self.msg_log("downloadProgress %s of %s ..." % (bytesReceived, bytesTotal)) + pass + + #@pyqtSlot(QNetworkReply) + def requestTimedOut(self, QNetworkReply): + """Trap the timeout. In Async mode requestTimedOut is called after replyFinished""" + # adapt http_call_result basing on receiving qgs timer timout signal + self.exception_class = RequestsExceptionTimeout + self.http_call_result.exception = RequestsExceptionTimeout("Timeout error") + + #@pyqtSlot(QObject) + def replyFinished(self): + err = self.reply.error() + httpStatus = self.reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) + httpStatusMessage = self.reply.attribute(QNetworkRequest.HttpReasonPhraseAttribute) + self.http_call_result.status_code = httpStatus + self.http_call_result.status = httpStatus + self.http_call_result.status_message = httpStatusMessage + for k, v in self.reply.rawHeaderPairs(): + self.http_call_result.headers[str(k)] = str(v) + self.http_call_result.headers[str(k).lower()] = str(v) + + if err != QNetworkReply.NoError: + # handle error + # check if errorString is empty, if so, then set err string as + # reply dump + if re.match('(.)*server replied: $', self.reply.errorString()): + errString = self.reply.errorString() + self.http_call_result.content + else: + errString = self.reply.errorString() + # check if self.http_call_result.status_code is available (client abort + # does not produce http.status_code) + if self.http_call_result.status_code: + msg = "Network error #{0}: {1}".format( + self.http_call_result.status_code, errString) + else: + msg = "Network error: {0}".format(errString) + + self.http_call_result.reason = msg + self.http_call_result.ok = False + self.msg_log(msg) + # set return exception + if err == QNetworkReply.TimeoutError: + self.http_call_result.exception = RequestsExceptionTimeout(msg) + + elif err == QNetworkReply.ConnectionRefusedError: + self.http_call_result.exception = RequestsExceptionConnectionError(msg) + + elif err == QNetworkReply.OperationCanceledError: + # request abort by calling NAM.abort() => cancelled by the user + if self.on_abort: + self.http_call_result.exception = RequestsExceptionUserAbort(msg) + else: + self.http_call_result.exception = RequestsException(msg) + + else: + self.http_call_result.exception = RequestsException(msg) + + # overload exception to the custom exception if available + if self.exception_class: + self.http_call_result.exception = self.exception_class(msg) + + else: + # Handle redirections + redirectionUrl = self.reply.attribute(QNetworkRequest.RedirectionTargetAttribute) + if redirectionUrl is not None and redirectionUrl != self.reply.url(): + if redirectionUrl.isRelative(): + redirectionUrl = self.reply.url().resolved(redirectionUrl) + + msg = "Redirected from '{}' to '{}'".format( + self.reply.url().toString(), redirectionUrl.toString()) + self.msg_log(msg) + + self.reply.deleteLater() + self.reply = None + self.request(redirectionUrl.toString()) + + # really end request + else: + msg = "Network success #{0}".format(self.reply.error()) + self.http_call_result.reason = msg + self.msg_log(msg) + + ba = self.reply.readAll() + self.http_call_result.content = bytes(ba) + self.http_call_result.ok = True + + # Let's log the whole response for debugging purposes: + #self.msg_log("Got response %s %s from %s" % \ + # (self.http_call_result.status_code, + # self.http_call_result.status_message, + # self.reply.url().toString())) + for k, v in list(self.http_call_result.headers.items()): + self.msg_log("%s: %s" % (k, v)) + if len(self.http_call_result.content) < 1024: + self.msg_log("Payload :\n%s" % self.http_call_result.content) + else: + self.msg_log("Payload is > 1 KB ...") + + # clean reply + if self.reply is not None: + if self.reply.isRunning(): + self.reply.close() + self.msg_log("Deleting reply ...") + # Disconnect all slots + self.reply.sslErrors.disconnect(self.sslErrors) + self.reply.finished.disconnect(self.replyFinished) + self.reply.downloadProgress.disconnect(self.downloadProgress) + self.reply.deleteLater() + self.reply = None + else: + self.msg_log("Reply was already deleted ...") + + #@pyqtSlot() + def sslErrors(self, ssl_errors): + """ + Handle SSL errors, logging them if debug is on and ignoring them + if disable_ssl_certificate_validation is set. + """ + if ssl_errors: + for v in ssl_errors: + self.msg_log("SSL Error: %s" % v.errorString()) + if self.disable_ssl_certificate_validation: + self.reply.ignoreSslErrors() + + #@pyqtSlot() + def abort(self): + """ + Handle request to cancel HTTP call + """ + if (self.reply and self.reply.isRunning()): + self.on_abort = True + self.reply.abort() \ No newline at end of file