From 7c0188b9a590ee9153219cdd385fe442a93ee95a Mon Sep 17 00:00:00 2001 From: Tom LAVEILLE Date: Fri, 2 Aug 2024 10:15:52 +0200 Subject: [PATCH] =?UTF-8?q?T=C3=A9l=C3=A9verser=20les=20fichiers=20vers=20?= =?UTF-8?q?"CenRa=5FFLUX"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CenRa_FLUX/FluxCEN.py | 898 ++++++++++++++++++++++++++++++++++++ CenRa_FLUX/__init__.py | 36 ++ CenRa_FLUX/arrow-bottom.png | Bin 0 -> 9378 bytes CenRa_FLUX/arrow-up.png | Bin 0 -> 9685 bytes CenRa_FLUX/cenra.png | Bin 0 -> 4977 bytes 5 files changed, 934 insertions(+) create mode 100644 CenRa_FLUX/FluxCEN.py create mode 100644 CenRa_FLUX/__init__.py create mode 100644 CenRa_FLUX/arrow-bottom.png create mode 100644 CenRa_FLUX/arrow-up.png create mode 100644 CenRa_FLUX/cenra.png diff --git a/CenRa_FLUX/FluxCEN.py b/CenRa_FLUX/FluxCEN.py new file mode 100644 index 00000000..31ad09ca --- /dev/null +++ b/CenRa_FLUX/FluxCEN.py @@ -0,0 +1,898 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + FluxCEN + A QGIS plugin + Centralisation des flux WFS/WMS utilisés au CEN NA + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2022-03-23 + git sha : $Format:%H$ + copyright : (C) 2022 by Romain MONTILLET + email : r.montillet@cen-na.org + ***************************************************************************/ + +/*************************************************************************** + * * + * 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 qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication, Qt, QUrl +from qgis.PyQt.QtGui import * +from qgis.PyQt.QtWidgets import * +from PyQt5 import * + +# Initialize Qt resources from file resources.py +from .resources import * +# Import the code for the dialog +from .FluxCEN_dialog import FluxCENDialog +import os.path, os, shutil +from qgis.core import * +from qgis.gui import * +from qgis.utils import * +import processing +import psycopg2 +import psycopg2.extras +from PyQt5.QtXml import QDomDocument +import csv +import os +import io +import re +import random +# Deal with SSL +import ssl +import urllib +from urllib import request, parse +import socket +import json +import requests +import base64 + +ssl._create_default_https_context = ssl._create_unverified_context + +from .tools.resources import maj_verif +from .tools.PythonSQL import * +first_conn = psycopg2.connect("host=" + host + " port=" + port + " dbname="+dbname+" user=first_cnx password=" + password) +first_cur = first_conn.cursor(cursor_factory = psycopg2.extras.DictCursor) +first_cur.execute("SELECT mdp_w, login_w FROM pg_catalog.pg_user t1, admin_sig.vm_users_sig t2 WHERE t2.oid = t1.usesysid AND (login_w = '" + os_user + "' OR login_w = '" + os_user + "')") +res_ident = first_cur.fetchone() +mdp = base64.b64decode(str(res_ident[0])).decode('utf-8') +user = res_ident[1] +#con = psycopg2.connect("host=" + host + " port=" + port + " dbname="+dbname+" user=" + user + " password=" + mdp) +#cur = con.cursor(cursor_factory = psycopg2.extras.DictCursor) +#from .HubToTea import gitea +#gitea() +first_conn.close() + + +''' +# Vérifier la connexion à internet +try: + # Vérifier si l'utilisateur est connecté à internet en ouvrant une connexion avec un site web + host = socket.gethostbyname("www.google.com") + s = socket.create_connection((host, 80), 2) + s.close() +except socket.error: + # Afficher un message si l'utilisateur n'est pas connecté à internet + QMessageBox.warning(None, 'Avertissement', + 'Vous n\'êtes actuellement pas connecté à internet. Veuillez vous connecter pour pouvoir utiliser FluxCEN !') +''' + +class Flux: + def __init__(self, t, c, nc, l, u, p): + self.type = t + self.category = c + self.nom_commercial = nc + self.layer = l + self.url = u + self.parameters = p + maj_verif('CenRa_FLUX') + + +class Popup(QWidget): + def __init__(self, parent=None): + super(Popup, self).__init__(parent) + + self.plugin_dir = os.path.dirname(__file__) + + self.text_edit = QTextBrowser() + fp = urllib.request.urlopen("https://raw.githubusercontent.com/CEN-Nouvelle-Aquitaine/fluxcen/main/info_changelog.html") + mybytes = fp.read() + html_changelog = mybytes.decode("utf8") + fp.close() + + self.text_edit.setHtml(html_changelog) + self.text_edit.setFont(QtGui.QFont("Calibri",weight=QtGui.QFont.Bold)) + self.text_edit.anchorClicked.connect(QtGui.QDesktopServices.openUrl) + self.text_edit.setOpenLinks(False) + + self.text_edit.setWindowTitle("Nouveautés") + self.text_edit.setMinimumSize(600,450) + +class FluxCEN: + """QGIS Plugin Implementation.""" + + def __init__(self, iface): + """Constructor. + + :param iface: An interface instance that will be passed to this class + which provides the hook by which you can manipulate the QGIS + application at run time. + :type iface: QgsInterface + """ + # Save reference to the QGIS interface + self.iface = iface + # initialize plugin directory + self.plugin_dir = os.path.dirname(__file__) + # initialize locale + #print(QSettings().value('locale/userLocale')) + locale = QSettings().value('locale/userLocale')[0:2] + locale_path = os.path.join( + self.plugin_dir, + 'i18n', + 'FluxCEN_{}.qm'.format(locale)) + + if os.path.exists(locale_path): + self.translator = QTranslator() + self.translator.load(locale_path) + QCoreApplication.installTranslator(self.translator) + + # Declare instance attributes + self.actions = [] + self.menu = self.tr(u'&FluxCEN') + self.dlg = FluxCENDialog() + + self.plugin_path = os.path.dirname(__file__) + + # Check if plugin was started the first time in current QGIS session + # Must be set in initGui() to survive plugin reloads + self.first_start = None + + self.dlg.tableWidget.setSelectionBehavior(QTableWidget.SelectRows) + self.dlg.tableWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + + self.dlg.comboBox_2.addItem("SIG") + self.dlg.comboBox_2.addItem('REF') +# self.dlg.comboBox.addItem('07') +# self.dlg.comboBox.addItem('26') +# self.dlg.comboBox.addItem('42') +# self.dlg.comboBox.addItem('69') +# self.dlg.comboBox.addItem('form') + + self.dlg.comboBox.currentIndexChanged.connect(self.initialisation_flux) + self.dlg.commandLinkButton.clicked.connect(self.selection_flux) + self.dlg.tableWidget.itemDoubleClicked.connect(self.selection_flux) + self.dlg.pushButton_2.clicked.connect(self.limite_flux) + self.dlg.commandLinkButton_2.clicked.connect(self.suppression_flux) + self.dlg.tableWidget_2.itemDoubleClicked.connect(self.suppression_flux) + self.dlg.comboBox_2.currentIndexChanged.connect(self.bd_source) + #self.dlg.commandLinkButton_3.clicked.connect(self.option_OSM) + #self.dlg.commandLinkButton_4.clicked.connect(self.option_google_maps) + + #self.dlg.commandLinkButton_5.clicked.connect(self.popup) + # iface.mapCanvas().extentsChanged.connect(self.test5) + +# url_open = urllib.request.urlopen("https://raw.githubusercontent.com/CEN-Rhone-Alpes/Plugin_QGIS/main/flux.csv") +# colonnes_flux = csv.DictReader(io.TextIOWrapper(url_open, encoding='utf8'), delimiter=';') + + #mots_cles = [row["categorie"] for row in colonnes_flux if row["categorie"]] + #categories = list(set(mots_cles)) + #categories.sort() + + #self.dlg.comboBox.addItems(categories) + layout = QVBoxLayout() + self.dlg.lineEdit.textChanged.connect(self.filtre_dynamique) + layout.addWidget(self.dlg.lineEdit) + self.dlg.lineEdit.mousePressEvent = self._mousePressEvent + + metadonnees_plugin = open(self.plugin_path + '/metadata.txt') + infos_metadonnees = metadonnees_plugin.readlines() + +# derniere_version = urllib.request.urlopen("https://sig.dsi-cen.org/qgis/downloads/last_version_fluxcen.txt") +# num_last_version = derniere_version.readlines()[0].decode("utf-8") + + # Connect the itemClicked signal to the open_url function + #self.dlg.tableWidget.itemClicked.connect(self.open_url) + +# version_utilisateur = infos_metadonnees[8].splitlines() + +# if infos_metadonnees[8].splitlines() == num_last_version.splitlines(): +# iface.messageBar().pushMessage("Plugin à jour", "Votre version de FluxCEN %s est à jour !" %version_utilisateur, level=Qgis.Success, duration=5) +# else: +# iface.messageBar().pushMessage("Information :", "Une nouvelle version de FluxCEN est disponible, veuillez mettre à jour le plugin !", level=Qgis.Info, duration=120) + + def _mousePressEvent(self, event): + self.dlg.lineEdit.setText("") + self.dlg.lineEdit.mousePressEvent = None + + # noinspection PyMethodMayBeStatic + def tr(self, message): + """Get the translation for a string using Qt translation API. + + We implement this ourselves since we do not inherit QObject. + + :param message: String for translation. + :type message: str, QString + + :returns: Translated version of message. + :rtype: QString + """ + # noinspection PyTypeChecker,PyArgumentList,PyCallByClass + return QCoreApplication.translate('FluxCEN', message) + + + def add_action( + self, + icon_path, + text, + callback, + dbtype=None, + enabled_flag=True, + add_to_menu=True, + add_to_toolbar=True, + status_tip=None, + whats_this=None, + parent=None): + """Add a toolbar icon to the toolbar. + + :param icon_path: Path to the icon for this action. Can be a resource + path (e.g. ':/plugins/foo/bar.png') or a normal file system path. + :type icon_path: str + + :param text: Text that should be shown in menu items for this action. + :type text: str + + :param callback: Function to be called when the action is triggered. + :type callback: function + + :param enabled_flag: A flag indicating if the action should be enabled + by default. Defaults to True. + :type enabled_flag: bool + + :param add_to_menu: Flag indicating whether the action should also + be added to the menu. Defaults to True. + :type add_to_menu: bool + + :param add_to_toolbar: Flag indicating whether the action should also + be added to the toolbar. Defaults to True. + :type add_to_toolbar: bool + + :param status_tip: Optional text to show in a popup when mouse pointer + hovers over the action. + :type status_tip: str + + :param parent: Parent widget for the new action. Defaults None. + :type parent: QWidget + :param whats_this: Optional text to show in the status bar when the + mouse pointer hovers over the action. + + :returns: The action that was created. Note that the action is also + added to self.actions list. + :rtype: QAction + """ + + icon = QIcon(icon_path) + action = QAction(icon, text, parent) + action.triggered.connect(callback) + action.setEnabled(enabled_flag) + + if status_tip is not None: + action.setStatusTip(status_tip) + + if whats_this is not None: + action.setWhatsThis(whats_this) + + if add_to_toolbar: + # Adds plugin icon to Plugins toolbar + #self.iface.addToolBarIcon(action) + self.toolBar.addAction(action) + + if add_to_menu: + self.iface.addPluginToMenu( + self.menu, + action) + + self.actions.append(action) + + return action + + def initGui(self): + self.toolBar = self.iface.addToolBar("FluxCEN") + self.toolBar.setObjectName("FluxCEN") + """Create the menu entries and toolbar icons inside the QGIS GUI.""" + icon_path = ':/plugins/CenRa_FLUX/reficon.png' + self.add_action( + icon_path, + text=self.tr(u'SigCEN'), + callback=self.bd_source, + dbtype='"+dbname+"', + parent=self.iface.mainWindow()) + ''' + icon_path_2 = ':/plugins/CenRa_FLUX/reficon.png' + self.add_action( + icon_path_2, + text=self.tr(u'RefCEN'), + callback=self.run_ref, + dbtype='ref_geo4269', + parent=self.iface.mainWindow()) + ''' + + # will be set False in run() + self.first_start = False + + def unload(self): + """Removes the plugin menu item and icon from QGIS GUI.""" + for action in self.actions: + self.iface.removePluginMenu( + self.tr(u'&SigCEN'), + action) + self.iface.removeToolBarIcon(action) + for action in self.actions: + self.iface.removePluginMenu( + self.tr(u'&RefCEN'), + action) + self.iface.removeToolBarIcon(action) + + def bd_source(self): + bd_origine=self.dlg.comboBox_2.currentText() + if bd_origine == 'REF': + self.run_ref() + if bd_origine == 'SIG': + self.run_sig() + + def run_ref(self): + """Run method that performs all the real work""" + while self.dlg.tableWidget_2.rowCount() > 0: + self.dlg.tableWidget_2.removeRow(self.dlg.tableWidget_2.rowCount()-1) +# print(self.dlg.tableWidget_2.rowCount()) + global cur,con,dbtype + dbtype=refdb + con = psycopg2.connect("host=" + host + " port=" + port + " dbname="+dbtype+" user=" + user + " password=" + mdp) + cur = con.cursor(cursor_factory = psycopg2.extras.DictCursor) + self.initialisation_flux() + self.combobox_custom() + # Create the dialog with elements (after translation) and keep reference + # Only create GUI ONCE in callback, so that it will only load when the plugin is started + if self.first_start == True: + self.first_start = False + # show the dialog + self.dlg.show() + # Run the dialog event loop + result = self.dlg.exec_() + # See if OK was pressed + if result: + # Do something useful here - delete the line containing pass and + # substitute with your code. + pass + + def run_sig(self): + """Run method that performs all the real work""" + while self.dlg.tableWidget_2.rowCount() > 0: + self.dlg.tableWidget_2.removeRow(self.dlg.tableWidget_2.rowCount()-1) + global cur,con,dbtype + dbtype=sigdb + con = psycopg2.connect("host=" + host + " port=" + port + " dbname="+dbtype+" user=" + user + " password=" + mdp) + cur = con.cursor(cursor_factory = psycopg2.extras.DictCursor) + self.initialisation_flux() + self.combobox_custom() + # Create the dialog with elements (after translation) and keep reference + # Only create GUI ONCE in callback, so that it will only load when the plugin is started + if self.first_start == True: + self.first_start = False + # show the dialog + self.dlg.show() + # Run the dialog event loop + result = self.dlg.exec_() + # See if OK was pressed + if result: + # Do something useful here - delete the line containing pass and + # substitute with your code. + pass + + def suppression_flux(self): + self.dlg.tableWidget_2.removeRow(self.dlg.tableWidget_2.currentRow()) + ''' + def option_OSM(self): + tms = 'type=xyz&url=https://tile.openstreetmap.org/{z}/{x}/{y}.png&zmax=19&zmin=0' + layer = QgsRasterLayer(tms, 'OSM', 'wms') + + if not QgsProject.instance().mapLayersByName("OSM"): + QgsProject.instance().addMapLayer(layer) + else: + QMessageBox.question(iface.mainWindow(), u"Fond OSM déjà chargé !", "Le fond de carte OSM est déjà chargé", QMessageBox.Ok) + + OSM_layer = QgsProject.instance().mapLayersByName("OSM")[0] + + root = QgsProject.instance().layerTreeRoot() + + # Move Layer + OSM_layer = root.findLayer(OSM_layer.id()) + myClone = OSM_layer.clone() + parent = OSM_layer.parent() + parent.insertChildNode(-1, myClone) + parent.removeChildNode(OSM_layer) + + + def option_google_maps(self): + tms = 'type=xyz&zmin=0&zmax=20&url=https://mt1.google.com/vt/lyrs%3Ds%26x%3D{x}%26y%3D{y}%26z%3D{z}' + layer = QgsRasterLayer(tms, 'Google Satelitte', 'wms') + + if not QgsProject.instance().mapLayersByName("Google Satelitte"): + QgsProject.instance().addMapLayer(layer) + else: + QMessageBox.question(iface.mainWindow(), u"Fond Google Sat' déjà chargé !", "Le fond de carte Google Satelitte est déjà chargé", QMessageBox.Ok) + + google_layer = QgsProject.instance().mapLayersByName("Google Satelitte")[0] + + root = QgsProject.instance().layerTreeRoot() + + # Move Layer + google_layer = root.findLayer(google_layer.id()) + myClone = google_layer.clone() + parent = google_layer.parent() + parent.insertChildNode(-1, myClone) + parent.removeChildNode(google_layer) + ''' + + def open_url(self, item): + url = item.data(Qt.UserRole) + #if url: + # Open the URL, you might use QDesktopServices.openUrl for this in a standalone PyQt application + # QDesktopServices.openUrl(QUrl(url)) + + def initialisation_flux(self): + ''' + def csv_import(url): + url_open = urllib.request.urlopen(url) + csvfile = csv.reader(io.TextIOWrapper(url_open, encoding='utf8'), delimiter=';') + #on ne lit pas la première ligne correspondant aux noms des colonnes avec next() + next(csvfile) + return csvfile; + + data = [] + data2 = [] + model = QStandardItemModel() + + raw = csv_import( + "https://raw.githubusercontent.com/CEN-Rhone-Alpes/Plugin_QGIS/main/flux.csv") + for row in raw: + data.append(row) + data2.append(row) + data = [k for k in data if self.dlg.comboBox.currentText() in k] + data.sort() + data2.sort() + items = [ + QStandardItem(field) + for field in row] + + model.appendRow(items) + + row=0 + data=['a1','a2','a3'] + data2=[['b1','c1','d1'],['b2','c2','d2'],['b3','c3','d3']] + if self.dlg.comboBox.currentText() == 'toutes les catégories': + # print(str(data2[0])) + # del data2[0] + # print(str(data2[0])) + nb_row = len(data2) + nb_col = len(data2[0]) + print(data2[row][2]) + self.dlg.tableWidget.setRowCount(nb_row) + self.dlg.tableWidget.setColumnCount(nb_col) + for row in range(nb_row): + for col in range(nb_col): + item = QTableWidgetItem(str(data2[row][col])) + # Access the value from the 6th column for the current row (style here) + value_from_2nd_column = str(data2[row][2]) + # Set tooltip for each row + tooltip = f"Nom du flux: {value_from_2nd_column}" + item.setToolTip(tooltip) + + # Check if the current column is the "Résumé des métadonnées" column + if col == 7: + # Set icon for the "Résumé des métadonnées" column + icon_path = self.plugin_path + '/info_metadata.png' # Replace 'path_to_your_icon.png' with the actual path to your icon + icon = QIcon(icon_path) + item.setIcon(icon) + # Store the URL in the item's data for later retrieval + url_from_6th_column = str(data2[row][7]) # Assuming the URL is in the next column + item.setData(Qt.UserRole, url_from_6th_column)''' + if dbtype == sigdb: + if self.dlg.comboBox.currentText() == 'toutes les catégories': + custom_list=schemaname_list + elif self.dlg.comboBox.currentText() == 'travaux': + custom_list="""(SELECT schemaname,tablename from pg_catalog.pg_tables + where schemaname like '"""+ str(self.dlg.comboBox.currentText()) +"""%' order by schemaname,tablename) UNION (SELECT schemaname,matviewname AS tablename FROM pg_catalog.pg_matviews where schemaname like '"""+ str(self.dlg.comboBox.currentText()) +"""%' order by schemaname,tablename) order by schemaname,tablename;""" + else: + custom_list="""(SELECT schemaname,tablename from pg_catalog.pg_tables + where schemaname like '\_"""+ str(self.dlg.comboBox.currentText()) +"""%' order by schemaname,tablename) UNION (SELECT schemaname,matviewname AS tablename FROM pg_catalog.pg_matviews where schemaname like '\_"""+ str(self.dlg.comboBox.currentText()) +"""%' order by schemaname,tablename) order by schemaname,tablename;""" + else: + if self.dlg.comboBox.currentText() == 'toutes les catégories': + custom_list=schemaname_list_ref + else: + custom_list="""SELECT schemaname,tablename from pg_catalog.pg_tables + where schemaname like '"""+ str(self.dlg.comboBox.currentText()) +"""' order by schemaname,tablename;""" + cur.execute(custom_list) + list_schema = cur.fetchall() + + self.dlg.tableWidget.setRowCount(len(list_schema)) + self.dlg.tableWidget.setColumnCount(3) + i=0 + for value in list_schema: + if dbtype == sigdb: + type_val = str(value[0])[1:3] + schema_name=str(value[0])[4:] + table_name=str(value[1]) + else: + type_val = '' + schema_name=str(value[0]) + table_name=str(value[1]) + if type_val == 'fo': + type_val=str(value[0])[1:5] + schema_name=str(value[0])[6:] + table_name=str(value[1]) + elif type_val == 'ra': + type_val='travaux' + schema_name=str(value[0]) + table_name=str(value[1]) + elif type_val != '00' and type_val != '01' and type_val != '07' and type_val != '26' and type_val != '42' and type_val != '69' and type_val != 'ra': + type_val='agregation' + schema_name=str(value[0]) + table_name=str(value[1]) + + item = QTableWidgetItem(type_val) + self.dlg.tableWidget.setItem(i,0,item) + item = QTableWidgetItem(schema_name) + self.dlg.tableWidget.setItem(i,1,item) + item = QTableWidgetItem(table_name) + self.dlg.tableWidget.setItem(i,2,item) + i=i+1 + self.dlg.tableWidget.setColumnWidth(0, 20) + self.dlg.tableWidget.setColumnWidth(1, 300) + self.dlg.tableWidget.setColumnWidth(2, 300) + self.dlg.tableWidget.setHorizontalHeaderLabels(["Code","Schema","Table"]) + ''' + else: + nb_row = len(data) + nb_col = len(data[0]) + self.dlg.tableWidget.setRowCount(nb_row) + self.dlg.tableWidget.setColumnCount(nb_col) + for row in range(nb_row): + for col in range(nb_col): + item = QTableWidgetItem(str(data[row][col])) + # Access the value from the 6th column for the current row (style here) + value_from_2nd_column = str(data[row][1]) + # Set tooltip for each row + tooltip = f"Nom du flux: {value_from_2nd_column}" + item.setToolTip(tooltip) + + # Check if the current column is the "Résumé des métadonnées" column + if col == 7: + # Set icon for the "Résumé des métadonnées" column + icon_path = self.plugin_path + '/metadata.png' # Replace 'path_to_your_icon.png' with the actual path to your icon + icon = QIcon(icon_path) + item.setIcon(icon) + # Store the URL in the item's data for later retrieval + url_from_6th_column = str(data2[row][7]) # Assuming the URL is in the next column + item.setData(Qt.UserRole, url_from_6th_column) + + self.dlg.tableWidget.setItem(row, col, item) + + self.dlg.tableWidget.setHorizontalHeaderLabels(["Service", "Catégorie", "Flux", "Nom technique", "Url d'accès", "Source", "Style", "Infos"]) + + self.dlg.tableWidget.setColumnWidth(0, 76) + self.dlg.tableWidget.setColumnWidth(1, 0) + self.dlg.tableWidget.setColumnWidth(2, 610) + self.dlg.tableWidget.setColumnWidth(3, 0) + self.dlg.tableWidget.setColumnWidth(4, 0) + self.dlg.tableWidget.setColumnWidth(5, 88) + self.dlg.tableWidget.setColumnWidth(6, 0) + self.dlg.tableWidget.setColumnWidth(7, 30) + + self.dlg.tableWidget.selectRow(0) + ''' + def selection_flux(self): + selected_row = 0 + selected_items = self.dlg.tableWidget.selectedItems() + + # Assuming you want to compare items in the first column for uniqueness + new_item_text = selected_items[2].text() + + if not self.item_already_exists(new_item_text): + self.dlg.tableWidget_2.insertRow(selected_row) + + for column in range(self.dlg.tableWidget.columnCount()): + cloned_item = selected_items[column].clone() + self.dlg.tableWidget_2.setHorizontalHeaderLabels(["Code","Schema", "Table"]) + self.dlg.tableWidget_2.setColumnCount(3) + self.dlg.tableWidget_2.setItem(selected_row, column, cloned_item) + + self.dlg.tableWidget_2.setColumnWidth(0, 50) + self.dlg.tableWidget_2.setColumnWidth(1, 300) + self.dlg.tableWidget_2.setColumnWidth(2, 300) + + + def item_already_exists(self, new_item_text): + # Assuming you want to compare items in the first column for uniqueness + existing_items = self.dlg.tableWidget_2.findItems(new_item_text, QtCore.Qt.MatchExactly) + + # Check if there are any existing items with the same text in the first column + return len(existing_items) > 0 + + + + def limite_flux(self): + + if self.dlg.tableWidget_2.rowCount() > 3: + self.QMBquestion = QMessageBox.question(iface.mainWindow(), u"Attention !", + "Le nombre de flux à charger en une seule fois est limité à 3 pour des questions de performances. Souhaitez vous tout de même charger les " + str( + self.dlg.tableWidget_2.rowCount()) + " flux sélectionnés ? (risque de plantage de QGIS)", + QMessageBox.Yes | QMessageBox.No) + if self.QMBquestion == QMessageBox.Yes: + self.chargement_flux() + + if self.QMBquestion == QMessageBox.No: + print("Annulation du chargement des couches") + + if self.dlg.tableWidget_2.rowCount() <= 3: + self.chargement_flux() + + def chargement_flux(self): + + managerAU = QgsApplication.authManager() + k = managerAU.availableAuthMethodConfigs().keys() + + def REQUEST(type): + switcher = { + 'WFS': "GetFeature", + 'WMS': "GetMap", + 'WMS+Vecteur': "GetMap", + 'WMS+Raster': "GetMap", + 'WMTS': "GetMap" + } + return switcher.get(type, "nothing") + + + def displayOnWindows(type, uri, name): + ''' + if type == 'WFS': + vlayer = QgsVectorLayer(uri, name, "WFS") + # vlayer.setScaleBasedVisibility(True) + QgsProject.instance().addMapLayer(vlayer) + + layers = QgsProject.instance().mapLayers() # dictionary + + # rowCount() This property holds the number of rows in the table + for row in range(self.dlg.tableWidget_2.rowCount()): + # item(row, 0) Returns the item for the given row and column if one has been set; otherwise returns nullptr. + _item = self.dlg.tableWidget_2.item(row, 2).text() + _legend = self.dlg.tableWidget_2.item(row, 6).text() + # print(_item) + # print(_legend) + + for layer in layers.values(): + if layer.name() == _item: + if len(_legend) > 1: + styles_url = 'https://raw.githubusercontent.com/CEN-Nouvelle-Aquitaine/fluxcen/main/styles_couches/' + _legend + '.qml' + + fp = urllib.request.urlopen(styles_url) + mybytes = fp.read() + + document = QDomDocument() + document.setContent(mybytes) + + res = layer.importNamedStyle(document) + layer.triggerRepaint() + + else: + print("Pas de style à charger pour cette couche") + + elif type == 'WMS' or type == 'WMS Raster' or type == 'WMS Vecteur' or type == 'WMTS': + rlayer = QgsRasterLayer(uri, name, "WMS") + QgsProject.instance().addMapLayer(rlayer) + else: + print("Unknown datatype !") + ''' + p = [] + + for row in range(0, self.dlg.tableWidget_2.rowCount()): + ## supression de la partie de l'url après le point d'interrogation + if dbtype == sigdb: + code = self.dlg.tableWidget_2.item(row,0).text() + schema = '_'+code+'_'+self.dlg.tableWidget_2.item(row,1).text() + if code == 'travaux' or code == 'agregation': + schema = self.dlg.tableWidget_2.item(row,1).text() + table = self.dlg.tableWidget_2.item(row,2).text()#.split("?", 1)[0] + if dbtype == refdb: +# code = self.dlg.tableWidget_2.item(row,0).text() + schema = self.dlg.tableWidget_2.item(row,1).text() + table = self.dlg.tableWidget_2.item(row,2).text()#.split("?", 1)[0] + uri = QgsDataSourceUri() + uri.setConnection(host ,port ,dbtype ,user ,mdp) + # nom du schéma à remplacer: "hydrographie" à supprimer et mettre "couches_collaboratives" lorsqu'on aura regroupé les couches à modifier dans un même schéma + uri.setDataSource(schema, table, "geom") + uri.setKeyColumn('gid') + # Chargement de la couche PostGIS + layer = QgsVectorLayer(uri.uri(), table, "postgres") + # Ajout de la couche au canevas QGIS + QgsProject.instance().addMapLayer(layer) + ''' + try: + service = re.search('SERVICE=(.+?)&VERSION', self.dlg.tableWidget_2.item(row,4).text()).group(1) + except: + service = '1.0.0' + try: + version = re.search('VERSION=(.+?)&REQUEST', self.dlg.tableWidget_2.item(row,4).text()).group(1) + except: + version = '1.0.0' + + if self.dlg.tableWidget_2.item(row,0).text() == 'WMS' or self.dlg.tableWidget_2.item(row,0).text() == 'WMS Vecteur' or self.dlg.tableWidget_2.item(row,0).text() == 'WMS Raster': + a = Flux( + self.dlg.tableWidget_2.item(row,0).text(), + self.dlg.tableWidget_2.item(row,1).text(), + self.dlg.tableWidget_2.item(row,2).text(), + self.dlg.tableWidget_2.item(row,3).text(), + "url="+url, + { + 'service': self.dlg.tableWidget_2.item(row,0).text(), + 'version': version, + 'crs': "EPSG:2154", + 'format' : "image/png", + 'layers': self.dlg.tableWidget_2.item(row,3).text()+"&styles" + } + ) + + p.append(a) + + uri = p[row].url + '&' + urllib.parse.unquote(urllib.parse.urlencode(p[row].parameters)) + # print(uri) + if not QgsProject.instance().mapLayersByName(p[row].nom_commercial): + displayOnWindows(p[row].type, uri, p[row].nom_commercial) + else: + print("Couche "+p[row].nom_commercial+" déjà chargée") + + + elif self.dlg.tableWidget_2.item(row,0).text() == 'WFS': + + a = Flux( + self.dlg.tableWidget_2.item(row, 0).text(), + self.dlg.tableWidget_2.item(row, 1).text(), + self.dlg.tableWidget_2.item(row, 2).text(), + self.dlg.tableWidget_2.item(row, 3).text(), + url, + { + 'VERSION': version, + 'TYPENAME': self.dlg.tableWidget_2.item(row, 3).text(), + 'request': "GetFeature", + + } + ) + + p.append(a) + + uri = p[row].url + '?' + urllib.parse.unquote(urllib.parse.urlencode(p[row].parameters)) + + try: + response = requests.get(uri) + + if response.status_code == 401: + print("Statut de réponse: 401") + + if len(list(k)) == 0: + QMessageBox.question(iface.mainWindow(), u"Attention", "Veuillez ajouter une entrée de configuration d'authentification dans QGIS pour accéder aux flux CEN-NA sécurisés par un mot de passe (Flux 'FoncierCEN')", QMessageBox.Ok) + else: + # Add 'authcfg' to the parameters dictionary + p[row].parameters['authcfg'] = list(k)[0] + + # Update the URI with the modified parameters + uri = p[row].url + '?' + urllib.parse.unquote(urllib.parse.urlencode(p[row].parameters)) + + # Make the request again with the updated URI + response = requests.get(uri) + elif response.status_code == 200: + print("Statut de réponse: 200.") + else: + print(f"Statut de réponse: {response.status_code}") + + except requests.exceptions.RequestException as e: + print(f"problème de requete: {e}") + + + if not QgsProject.instance().mapLayersByName(p[row].nom_commercial): + displayOnWindows(p[row].type, uri, p[row].nom_commercial) + else: + print("Couche "+p[row].nom_commercial+" déjà chargée") + + + elif self.dlg.tableWidget_2.item(row, 0).text() == 'PostGIS': + + # Connexion à la base de données PostGIS + uri = QgsDataSourceUri() + uri.setConnection("sandbox.cen-nouvelle-aquitaine.dev", "5432", "piezo", "", "") + # nom du schéma à remplacer: "hydrographie" à supprimer et mettre "couches_collaboratives" lorsqu'on aura regroupé les couches à modifier dans un même schéma + uri.setDataSource("collaboratif", self.dlg.tableWidget_2.item(row, 3).text(), "geom") + # Chargement de la couche PostGIS + layer = QgsVectorLayer(uri.uri(), self.dlg.tableWidget_2.item(row, 2).text(), "postgres") + + # Ajout de la couche au canevas QGIS + QgsProject.instance().addMapLayer(layer) + + else: + print("Les flux WMTS et autres ne sont pas encore gérés par le plugin") + ''' + def combobox_custom(self): + if dbtype == sigdb: + self.dlg.comboBox.clear() + self.dlg.comboBox.addItem("toutes les catégories") + self.dlg.comboBox.addItem('00') + self.dlg.comboBox.addItem('01') + self.dlg.comboBox.addItem('07') + self.dlg.comboBox.addItem('26') + self.dlg.comboBox.addItem('42') + self.dlg.comboBox.addItem('69') + self.dlg.comboBox.addItem('agregation') + self.dlg.comboBox.addItem('travaux') + self.dlg.comboBox.addItem('form') + if dbtype == refdb: + custom_list=schemaname_distinct + cur.execute(custom_list) + list_schema = cur.fetchall() + self.dlg.comboBox.clear() + self.dlg.comboBox.addItem("toutes les catégories") + for baxval in list_schema: + self.dlg.comboBox.addItem(baxval[0]) + def filtre_dynamique(self, filter_text): + + for i in range(self.dlg.tableWidget.rowCount()): + for j in range(self.dlg.tableWidget.columnCount()): + item = self.dlg.tableWidget.item(i, j) + match = filter_text.lower() not in item.text().lower() + self.dlg.tableWidget.setRowHidden(i, match) + if not match: + break + + + + def popup(self): + + self.dialog = Popup() # +++ - self + self.dialog.text_edit.show() + +# from owslib.wfs import WebFeatureService +# import csv + +# wfs = WebFeatureService(url='https://opendata.cen-nouvelle-aquitaine.org/geoserver/agriculture/wfs') +# agriculture = list(wfs.contents) +# with open('C:/Users/Romain/Desktop/test.csv', "a+", encoding="ISO-8859-1", newline='') as f: +# writer = csv.writer(f) +# for row in agriculture: +# writer.writerow(row.split()) +# +# from owslib.wms import WebMapService +# wms = WebMapService('https://opendata.cen-nouvelle-aquitaine.org/geoserver/fond_carto/wms') +# fonds_carto = list(wms.contents) +# with open('C:/Users/Romain/Desktop/test.csv', "a+", encoding="ISO-8859-1", newline='') as f: +# writer = csv.writer(f) +# for row in fonds_carto: +# writer.writerow(row.split()) +# +# import csv +# +# fluxWMS = ['AGG_TMM', '16-014_Brandes_de_Soyaux_2020-05', '17IMERIS_Bois-Charles_Vallée-du-Larry_2022_01', '17IMERIS_Grand-Champ_2022-01', '19PTOR_MNS_filtre_futurs_travaux_2021-10_L93', '19PTOR_MNS_filtre_travaux_realises_2021-10_L93', '19PTOR_ortho_2021-10_L93', '23CELI_marais_du_chancelier_2022_03_24', '23CLAM_Rocher_de_Clamouzat_2020-11', '23DIAB_lande_du_pont_du_diable_nord_2021-10_L93', '23DIAB_lande_du_pont_du_diable_sud_2021-10_L93', '23LAND_RNN_etang_des_landes_2020-08_L93', '33_Lagune-108-2021-08', '79BLVI_Blanchère-de-Viennay_2021-10', '79VGAT_Vallée-du-Gâteau_Pressigny_2020-02', '79VGAT_Vallée-du-Gâteau_Pressigny_2021-10', '86-001_TMM_CA-CD_2020-07', '86-500_Clain-sud_Etang-du-Pin', '86_AT_Chalandray_2021-10', '87CREN_siege_saint_gence_2021-09', '87GRLA_grandes_landes_2021-09-24', '87SANA_sanadie_2021-09-24', 'a_16_030_Prairies_de_Vouharte_2019_09', 'a_17_474_Estauaire_de_la_Gironde_Les_Pr_s_de_la_Rouille_2019_08', 'a_17_474_Estauaire_de_la_Gironde_Moulin_Rompu_2019_08', 'a_17_474_Estuaire_de_la_Gironde_Zone_Humide_de_la_Motte_Ronde_2021_04', 'a_17_IMERIS_Carriere_du_Planton_2021_08_12', 'a_17_LGV_Ragouillis_2021_08_12', 'a_33_Lagune_058_2021_08', 'a_33_Lagune_070_2021_08', 'a_33_Lagune_094_2021_08', 'a_33_Lagune_162_2021_08', 'a_33_Lagune_165_2021_08', 'a_33_Lagunes_207_208_209_2021_08', 'a_79_001_Clussais_la_Pommeraie_2020_11', 'a_79_008_Landes_de_L_Hopiteau_2019_09', 'a_79_020_Bessines_1_avant_travaux_2019_10', 'a_79_020_Bessines_2_pendant_travaux_2019_11', 'a_79_020_Bessines_3_apres_travaux_2020_12', 'a_79_044_Carriere_des_Landes_2020_09', 'a_79_AT_Vernoux_en_Gatine_2020_09', 'a_79_Sources_de_la_Sevre_Niortaise_Pierre_levee_2020_09', 'a_86_001_TMM_AA_2020_06', 'a_86_001_TMM_AB_2020_06', 'a_86_001_TMM_AC_2020_06', 'a_86_001_TMM_AD_2020_06', 'a_86_001_TMM_AE_2020_06', 'a_86_001_TMM_AF_2020_06', 'a_86_001_TMM_AG_2020_07', 'a_86_001_TMM_BA_2020_06', 'a_86_001_TMM_BB_2020_06', 'a_86_001_TMM_BC_2020_06', 'a_86_001_TMM_BD_2020_06', 'a_86_001_TMM_BE_2020_07', 'a_86_001_TMM_BF_2021_06', 'a_86_001_TMM_CB_2020_07', 'a_86_001_TMM_CC_2020_07', 'a_86_001_TMM_CC_2021_06', 'a_86_001_TMM_CD_2021_06', 'a_86_001_TMM_CE_2020_07', 'a_86_001_TMM_CF_2020_09', 'a_86_001_TMM_DA_2020_07', 'a_86_001_TMM_DB_2020_09', 'a_86_001_TMM_DC_2021_06', 'a_86_001_TMM_EA_2020_06', 'a_86_001_TMM_EB_2020_06', 'a_86_001_TMM_EC_2020_06', 'a_86_001_TMM_FA_2020_06', 'a_86_001_TMM_FB_2020_07', 'a_86_001_TMM_FC_2020_07', 'a_86_001_TMM_FC_2021_06', 'a_86_001_TMM_HA_2020_09', 'a_86_001_TMM_IA_2020_06', 'a_86_001_TMM_IB_2020_06', 'a_86_001_TMM_IC_2020_06', 'a_86_001_TMM_JA_2020_07', 'a_86_001_TMM_JB_2020_07', 'a_86_001_TMM_JC_2020_07', 'a_86_001_TMM_JE_2020_07', 'a_86_001_TMM_KA_2020_07', 'a_86_001_TMM_KB_2020_07', 'a_86_003_Falunieres_de_Moulin_Pochas_2019_09', 'a_86_006_Landes_et_pelouses_de_Lussac_Sillars_2019_08', 'a_86_011_Landes_de_Sainte_Marie_2019_09', 'a_86_025_Marais_des_Ragouillis_2020_11', 'a_86_025_Marais_des_Ragouillis_2021_02', 'a_86_026_Etangs_Baro_2019_09', 'a_86_029_Vallee_de_la_Longere_2019_09', 'a_86_037_Tourbiere_des_Regeasses_2021_06', 'a_86_038_Vallees_de_la_Vienne_et_du_Clain_Persac_2019_09', 'a_86_038_Vallees_de_la_Vienne_et_du_Clain_Persac_2020_12', 'a_86_052_Fontaine_le_Comte_nord_2020_11', 'a_86_052_Fontaine_le_Comte_sud_2020_11', 'a_86_054_Vallee_de_la_Vonne_2020_11', 'a_86_058_Carriere_de_Puy_Herve_2021_02_09', 'a_86_058_Carriere_de_Puy_Herve_2021_02_25', 'a_86_060_Bocage_de_la_Geoffronniere_2020_11', 'a_86_Le_Cormier_2021_05'] +# +# with open('C:/Users/Romain/Desktop/test.csv', "a+", encoding="ISO-8859-1", newline='') as f: +# writer = csv.writer(f) +# for row in fluxWMS: +# writer.writerow(row.split()) + +#### Récupération des métadonnées des couches quand disponibles: + +# from owslib.wms import WebMapService +# wms = WebMapService('http://geoservices.brgm.fr/geologie?service=WMS+Raster', version='1.1.1') +# print(list(wms.contents)) +# +# print(wms['IDPR'].abstract) \ No newline at end of file diff --git a/CenRa_FLUX/__init__.py b/CenRa_FLUX/__init__.py new file mode 100644 index 00000000..d9f00200 --- /dev/null +++ b/CenRa_FLUX/__init__.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + FluxCEN + A QGIS plugin + Flux IGN etc etc + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2022-04-04 + copyright : (C) 2022 by Romain Montillet + email : r.montillet@cen-na.org + git sha : $Format:%H$ + ***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + This script initializes the plugin, making it known to QGIS. +""" + + +# noinspection PyPep8Naming +def classFactory(iface): # pylint: disable=invalid-name + """Load FluxCEN class from file FluxCEN. + + :param iface: A QGIS interface instance. + :type iface: QgsInterface + """ + # + from .FluxCEN import FluxCEN + return FluxCEN(iface) diff --git a/CenRa_FLUX/arrow-bottom.png b/CenRa_FLUX/arrow-bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..845e482126c8940d4e7b47e15d7899343a0414af GIT binary patch literal 9378 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 literal 0 HcmV?d00001