From ad3e5b4fde22d6c591cbe40e91a39eab6ed680f4 Mon Sep 17 00:00:00 2001 From: eguilley Date: Wed, 29 Apr 2020 15:27:06 +0200 Subject: [PATCH 01/19] =?UTF-8?q?Impl=C3=A9mentation=20algorithme=20Histog?= =?UTF-8?q?ram?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __pycache__/histogram.cpython-36.pyc | Bin 0 -> 6056 bytes .../scripts_lpo_provider.cpython-36.pyc | Bin 3475 -> 3527 bytes histogram.py | 184 ++++++++++++++++++ icons/extract_data.png | Bin 54307 -> 5545 bytes icons/histogram.png | Bin 0 -> 3108 bytes scripts_lpo_provider.py | 3 +- 6 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 __pycache__/histogram.cpython-36.pyc create mode 100644 histogram.py create mode 100644 icons/histogram.png diff --git a/__pycache__/histogram.cpython-36.pyc b/__pycache__/histogram.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..787591cd32149483f4e1f518842866d44b9ab1ee GIT binary patch literal 6056 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 diff --git a/histogram.py b/histogram.py new file mode 100644 index 0000000..9dc1a1a --- /dev/null +++ b/histogram.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- + +""" +/*************************************************************************** + ScriptsLPO : graph.py + ------------------- + Date : 2020-04-16 + Copyright : (C) 2020 by Elsa Guilley (LPO AuRA) + Email : lpo-aura@lpo.fr + ***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ +""" + +__author__ = 'Elsa Guilley (LPO AuRA)' +__date__ = '2020-04-16' +__copyright__ = '(C) 2020 by Elsa Guilley (LPO AuRA)' + +# This will get replaced with a git SHA1 when you do a git archive +__revision__ = '$Format:%H$' + +import os +from qgis.PyQt.QtGui import QIcon + +from qgis.PyQt.QtCore import QCoreApplication +from qgis.core import (QgsProcessing, + QgsProcessingAlgorithm, + QgsProcessingParameterString, + QgsProcessingParameterVectorLayer, + QgsProcessingOutputVectorLayer, + QgsDataSourceUri, + QgsVectorLayer, + QgsWkbTypes, + QgsProcessingContext, + QgsProcessingException) +from qgis.utils import iface +from processing.tools import postgis + +import processing +import matplotlib.pyplot as plt +import numpy as np + +pluginPath = os.path.dirname(__file__) + + +class Histogram(QgsProcessingAlgorithm): + """ + This algorithm takes a connection to a data base and a vector polygons layer and + returns a summary non geometric PostGIS layer. + """ + + # Constants used to refer to parameters and outputs + DATABASE = 'DATABASE' + ZONE_ETUDE = 'ZONE_ETUDE' + OUTPUT = 'OUTPUT' + + def name(self): + return 'Histogram' + + def displayName(self): + return 'Create an histogram' + + def icon(self): + return QIcon(os.path.join(pluginPath, 'icons', 'histogram.png')) + + def groupId(self): + return 'treatments' + + def group(self): + return 'Treatments' + + def initAlgorithm(self, config=None): + """ + Here we define the inputs and output of the algorithm, along + with some other properties. + """ + + # Data base connection + db_param = QgsProcessingParameterString( + self.DATABASE, + self.tr('Nom de la connexion à la base de données') + ) + db_param.setMetadata( + { + 'widget_wrapper': {'class': 'processing.gui.wrappers_postgis.ConnectionWidgetWrapper'} + } + ) + self.addParameter(db_param) + + # Input vector layer = study area + self.addParameter( + QgsProcessingParameterVectorLayer( + self.ZONE_ETUDE, + self.tr("Zone d'étude"), + [QgsProcessing.TypeVectorAnyGeometry] + ) + ) + + # Output PostGIS layer + # self.addOutput( + # QgsProcessingOutputVectorLayer( + # self.OUTPUT, + # self.tr('Couche en sortie'), + # QgsProcessing.TypeVectorAnyGeometry + # ) + # ) + + def processAlgorithm(self, parameters, context, feedback): + """ + Here is where the processing itself takes place. + """ + + # Retrieve the input vector layer = study area + zone_etude = self.parameterAsVectorLayer(parameters, self.ZONE_ETUDE, context) + # Initialization of the "where" clause of the SQL query, aiming to retrieve the data for the histogram + where = "and (" + # For each entity in the study area... + for feature in zone_etude.getFeatures(): + # Retrieve the geometry + area = feature.geometry() # QgsGeometry object + # Retrieve the geometry type (single or multiple) + geomSingleType = QgsWkbTypes.isSingleType(area.wkbType()) + # Increment the "where" clause + if geomSingleType: + where = where + "st_within(geom, ST_PolygonFromText('{}', 2154)) or ".format(area.asWkt()) + else: + where = where + "st_within(geom, ST_MPolyFromText('{}', 2154)) or ".format(area.asWkt()) + # Remove the last "or" in the "where" clause which is useless + where = where[:len(where)-4] + ")" + #feedback.pushInfo('Clause where : {}'.format(where)) + + query = """(select groupe_taxo, count(*) as nb_observations + from src_lpodatas.observations + where is_valid {} + group by groupe_taxo + order by count(*) desc)""".format(where) + + # Retrieve the data base connection name + connection = self.parameterAsString(parameters, self.DATABASE, context) + # Retrieve the data for the histogram through a layer + # URI --> Configures connection to database and the SQL query + uri = postgis.uri_from_name(connection) + uri.setDataSource("", query, None, "", "groupe_taxo") + layer_histo = QgsVectorLayer(uri.uri(), "Histogram", "postgres") + + # Check if the PostGIS layer is valid + if not layer_histo.isValid(): + raise QgsProcessingException(self.tr("""Cette couche n'est pas valide ! + Checker les logs de PostGIS pour visualiser les messages d'erreur.""")) + else: + feedback.pushInfo('La couche PostGIS demandée est valide, la requête SQL a été exécutée avec succès !') + + + libel = [feature['groupe_taxo'] for feature in layer_histo.getFeatures()] + feedback.pushInfo('Libellés : {}'.format(libel)) + X = np.arange(len(libel)) + feedback.pushInfo('Valeurs en X : {}'.format(X)) + Y = [int(feature['nb_observations']) for feature in layer_histo.getFeatures()] + feedback.pushInfo('Valeurs en Y : {}'.format(Y)) + # fig = plt.figure() + # ax = fig.add_axes([0, 0, 1, 1]) + plt.rcdefaults() + fig, ax = plt.subplots() + ax.bar(X, Y, align='center', color='blue', ecolor='black') + ax.set_xticks(X) + ax.set_xticklabels(libel) + ax.set_ylabel(u'Nombre d\'observations') + ax.set_title(u'Etat des connaissances par groupes d\'espèces') + plt.show() + + return {} + + def tr(self, string): + return QCoreApplication.translate('Processing', string) + + def createInstance(self): + return Histogram() diff --git a/icons/extract_data.png b/icons/extract_data.png index 4f72703ba0c55268104942f7a3be69b5db8af317..3af85e2fcc42809cb55689a2a74adcef8df38697 100644 GIT binary patch literal 5545 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 diff --git a/icons/histogram.png b/icons/histogram.png new file mode 100644 index 0000000000000000000000000000000000000000..5a4a6c870cc62dff547b3f3f28945a40319360a8 GIT binary patch literal 3108 zcmeAS@N?(olHy`uVBq!ia0y~yV7>yvEX+WWeZO+H11XktM_)$E)e-c@Ne8%D+ zcPEB*=VV?2ISR=hLB0$ORcZ_j4J`}|zkoswFBlj~4Hy_+B``2p&0t^J_0XY1GiT0x`0(MTO`D!ReQIQ6bmq*N`uh6)`}g0xdGph!PZ}B; zK*h&+D$7*M~^tyk)6^TK~l zy;7#H*q#5jN&$89xBcR#jR70(3w+)GXG8G6xBsg^HVsBM4tyw)H4*Rv0fl*Frn z`BPDk^Uv7nz?P_LiEBhjN@7W>RdP`(kYX@0FtpS)Fw-@%3^6paGBL6;G}bn-ure?( zWbn;K(U6;;l9^Ts(qN`*V4`bi7Gh{>Wo%?+Yyr`rm^mHT(q;$&*-%`TZk3c+oT^(| zl*y2mnUiXzudknxpPQ Date: Thu, 30 Apr 2020 17:43:27 +0200 Subject: [PATCH 02/19] Optimisation de l'algorithme Extract Data --- __pycache__/extract_data.cpython-36.pyc | Bin 5335 -> 6029 bytes __pycache__/histogram.cpython-36.pyc | Bin 6056 -> 5944 bytes extract_data.py | 30 ++++++++++++++++++------ histogram.py | 26 +++++++------------- 4 files changed, 31 insertions(+), 25 deletions(-) diff --git a/__pycache__/extract_data.cpython-36.pyc b/__pycache__/extract_data.cpython-36.pyc index e052107b29827bfb790acb4e805d8acd8ba0b055..30afa67c126632f4d6125cd72d28eeac71fff620 100644 GIT binary patch delta 2121 zcmb_dO>Epm6!zF&uXoqppJad2v~iQrWT{A@DdJb5X_JQ1A0?ze>IxdU>&eEBf3{;g zbc0txT{u=EGN+0Q#~{>8goFf_s+>6?Az2)dda4iyuAF$|4I!Y?6T9-$d*8e_?`Ph; z`Kf;I#Mrbfr?-Cp^5$np80Il^;Ku`h8b0@{?XlTG#ApdEZAzBhkYhe$ZacLLR;f|4 z%8jyBX;iFgqiWR}wRl-Fhpa=5Lx{l%W7s&1kMvMuWP#CSE&DN}WpTd8H55%)X6iZh zF~>0J6Wb5Z?}`ZVFC_F;wTbH}l9yWUCG5It3!hO50Qse}4#BhSwy8H&Pj~EkCLB)u zkl>eEy1V-%aRH^5uUwtGu`>Ja{A@Ure7-aQ3G6Un%DdQHC1nsvIqW7aG!y=sT$`jo z9g#|m?~^KUQAX3a9rBZmqOcqFQ^o zQoT4GS+{j>)@(UM_tvaSp)OtA%E77t-&y$F?-9cUjKKs5C<@pf*X2Fd-~!&@drS{$ z)%7HK+e;Wik1>)Or}3U>q@XK=dpu&iG+1RczK43~E&>|~v}E|QJw{8ebL8#;OR;ac z^$%0AZ|dd1I3GJqt@GsbzSK?FB5;Ks(}gzd3U@9Bi8l=KyTY8;9fI2cdpaAQ?w`%%{j*+pABppQN1*IAf%t zm3jy<0c$8wsH2dD9LUC~Rt6{8APZ*qK_1gmfUtn{^~M9Kg|teK1x8T$-s>-HgV?~>IwBz|2R#qLK z*g9713G8}GTXmHU)zo3(sB#Vax@qD~#eKA6;wHU+HYSP>=1s?Mu3=>ZP}o@kb+6Oj z!b-az0G@h68@A3y2@`K%6S~wV@de&Y%`Yx5yngbfmrwcn{~XWg-vOx|)%GA#W#8Kr zfp%xYne4Z$|551VO3CJ$N?MP0LLqk?ZH6avySa09mYe`B@*)j>GW;!9+lud?400sz z&4*===yoegez1P)N~etp3^OK5Tl;e>%CvoVZEubddgUVVEL=huVJ69gNG2#IX{x6# z5@!R7O(L#I+(=M;Z%x-C&ct?+4>)lrIW{)M+7H&yQ9l>YDS3PHL1(>h3jq+~I@#vP8#A#Ohi- zM=GIQ7%#cdA)OVKU^Pm`(a;tM-zZF@%b`=4$n>Wx-MkupQ7FB78(8-Y5Jt$e1E`9! zY%!*Jlx9ViXDQ9HJSwtfw#etXG|HnPE*<_{(5Dk%7NtC*+ODa37>I@}%{k>*4$mg+}|6@EAP%6r4G6yU+BB_8UlyfflUOces&1Xhk1E(yvbpH$e_xD^5xz3%YTOk$iZwSZ)=le3&{& zYC5suC5yzBl~vuuD=TD#F4AzHmPaJMX*rs2;u%^SmsW&_lY&3Tvm%n{pOw(53{dV* DnwKIW delta 1478 zcmaJ>TW{P%6rS;Iy=h*PDgwhJQqlYZkSf7*BoO6^H=cMblEqua3%`Oh4k}euYHL27`F!Twe9nA*?T^;% zl4Tih|MK0(-!>6?hF*9SkZ;0@9(^;r@*prBGq4;hC_Cj~%oz)8#||n^B^YUt*qOS8NQszVB4ScAm7Q}$+eIzQeI_GhT;qqz=II>9 zO4Z(3?K8SE81%f38+-k*W$+Kw-%l@Vx3S^8zqQ_8zk6?OogWw8tdBuxKcE)Ql!*5B zSrxXd%6~2d_#J*lKe#H07G|}ae-3q!vJ&y4LC-zfas#@|f71EXtF#oa(+^iTDQ?ZZ z##W)r`oqB{S>sQOJC|OgsOE~dPB)ETv8IWDY;K#grGgME0u;V%Ubb{0jsvu0R^x|e zW9BS1LFdt1Ug*Uuy>6d*@j-B#KQ`~2l;NuiYZX>>9V3(=A0-$NCsHbp6oN7n8A6f}@syE&-DuA%7ZNollX6p&@PlTX~%?*iWouwo2oezR~FqI|p2= z%wxu{S58kDlooBYX0pb>Wzan_Wh>Er+KKz@j(bE|{v)$em&O~^jfadznck&+P-I7} z1ZOkTi*~)R+oN}n1~e;t`tXxn(DwT*aN|sMqrHc5CiiHVRbKG18#6dnU^dubW&YRr zrHh6zCl8s&8bT^~(S5h)5oQUMIRHLW-8DrgWs*l)W$30+bJomB z|6RSZ@luP~1J>`*D0<#yAy|nP0gz@(id2?#SPHI54XLKsvVmAZjo0xk-e7qBZp)P(H`_%GdD5@W@L|3yC2xM1GU<$TwgW)Jvg;|6Z> z&l}CR9Q&~AMGMfS*+G6_0yGvOgP<8!w}BGDmoBPQda1LWa>;IgJdV&=Rnc-X@jQCRiRAnHcVB#~(< zM5)hFQglT#k zl7%1PUCMAarngR}1=!dTUV@X@$12;g9G|nVh+flrmecivHKX3Mbw8vLQr52ZuFqCA z8%lBkOW?}2WRWiL!ZjGC4e(v7hsTcHwTS2d|5bXQ`!l{*@Cy1KO%`-=6@xfU^6@GPWhxDKv!_tJ~{Gm0!a`tS5c zcqZJL2tB50PPy3EX9X2npGEOjPZe6khJ#dS@raGM(CLifwX=ppm^z 0: + selection = study_area.selectedFeatures() # Get only the selected features + else: + selection = study_area.getFeatures() # If there is no feature selected, get all of them # Initialization of the "where" clause of the SQL query, aiming to retrieve the output PostGIS layer where = "" # For each entity in the study area... - for feature in zone_etude.getFeatures(): + for feature in selection: # Retrieve the geometry area = feature.geometry() # QgsGeometry object # Retrieve the geometry type (single or multiple) @@ -137,12 +153,12 @@ def processAlgorithm(self, parameters, context, feedback): # URI --> Configures connection to database and the SQL query uri = postgis.uri_from_name(connection) uri.setDataSource("src_lpodatas", "observations", "geom", where) - layer_obs = QgsVectorLayer(uri.uri(), "Données d'observations {}".format(zone_etude.name()), "postgres") + layer_obs = QgsVectorLayer(uri.uri(), "Données d'observations {}".format(study_area.name()), "postgres") # Check if the PostGIS layer is valid if not layer_obs.isValid(): - raise QgsProcessingException(self.tr("""Cette couche n'est pas valide ! - Checker les logs de PostGIS pour visualiser les messages d'erreur.""")) + raise QgsProcessingException(self.tr("""La couche PostGIS chargée n'est pas valide ! + Checkez les logs de PostGIS pour visualiser les messages d'erreur.""")) else: feedback.pushInfo('La couche PostGIS demandée est valide, la requête SQL a été exécutée avec succès !') diff --git a/histogram.py b/histogram.py index 9dc1a1a..728ad55 100644 --- a/histogram.py +++ b/histogram.py @@ -103,15 +103,6 @@ def initAlgorithm(self, config=None): ) ) - # Output PostGIS layer - # self.addOutput( - # QgsProcessingOutputVectorLayer( - # self.OUTPUT, - # self.tr('Couche en sortie'), - # QgsProcessing.TypeVectorAnyGeometry - # ) - # ) - def processAlgorithm(self, parameters, context, feedback): """ Here is where the processing itself takes place. @@ -157,19 +148,18 @@ def processAlgorithm(self, parameters, context, feedback): else: feedback.pushInfo('La couche PostGIS demandée est valide, la requête SQL a été exécutée avec succès !') - + plt.rcdefaults() libel = [feature['groupe_taxo'] for feature in layer_histo.getFeatures()] feedback.pushInfo('Libellés : {}'.format(libel)) - X = np.arange(len(libel)) - feedback.pushInfo('Valeurs en X : {}'.format(X)) + #X = np.arange(len(libel)) + #feedback.pushInfo('Valeurs en X : {}'.format(X)) Y = [int(feature['nb_observations']) for feature in layer_histo.getFeatures()] feedback.pushInfo('Valeurs en Y : {}'.format(Y)) - # fig = plt.figure() - # ax = fig.add_axes([0, 0, 1, 1]) - plt.rcdefaults() - fig, ax = plt.subplots() - ax.bar(X, Y, align='center', color='blue', ecolor='black') - ax.set_xticks(X) + fig = plt.figure() + ax = fig.add_axes([0, 0, 1, 1]) + # fig, ax = plt.subplots() + ax.bar(libel, Y) + # ax.set_xticks(X) ax.set_xticklabels(libel) ax.set_ylabel(u'Nombre d\'observations') ax.set_title(u'Etat des connaissances par groupes d\'espèces') From 55182acc9192cb7c48617b9229fbd01d7d4a7904 Mon Sep 17 00:00:00 2001 From: eguilley Date: Mon, 4 May 2020 11:45:07 +0200 Subject: [PATCH 03/19] Avancements --- __pycache__/common_functions.cpython-36.pyc | Bin 0 -> 1655 bytes __pycache__/extract_data.cpython-36.pyc | Bin 6029 -> 6067 bytes __pycache__/summary_table.cpython-36.pyc | Bin 6204 -> 7678 bytes common_functions.py | 35 +++++ extract_data.py | 38 ++++-- summary_table.py | 138 ++++++++++++++------ 6 files changed, 157 insertions(+), 54 deletions(-) create mode 100644 __pycache__/common_functions.cpython-36.pyc create mode 100644 common_functions.py diff --git a/__pycache__/common_functions.cpython-36.pyc b/__pycache__/common_functions.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a448e50a2efc3eed11a265cab87e2f183d56be04 GIT binary patch literal 1655 zcmb_cOK%)S5T2PGJI)XvkRovbsgP~(F6@qR5QlXhj!f(zMJ8Z!*@O)3>7LzodZtI+ z(}~7$axp)FAAkTUFNttKAOsSUNBoHqCvxCFaH4v%yI4G;2)RaGQ&nC4RrRCl)a-2V z*>8`p|2pM3Pn<*Kp}dMi12XFL9P0L5>h(O@=ryR{^XU|w{?zGB(PLXqbB6rhaGW4) z9UTQQqODl*ywH~}zYkrAWtz$?N=z0PTxPnHmlMbKKWQ@eE-Ba;aPsfM^3w8Bd+F`= zTklLf*JNHQ-X9bbHx#Ln0;Mz% zlMEE2To;OO8@w=H0Lf@56{M2#qy!j&;e^R3Q&0>T6ilTWWHMAXHm|@2%a|epE}Lz^ zW4Od)mT3kA<6Da!Flw)H3}!LbZ8lq@Y+zj?I)#K{h>bvTP>c{G*U%sz?uI}A!Lw%AE!jL$Z(il^gaTG;n z4%aWg_WI)0+k1CDZhmt8TDbZ~`%LTLoB4w;7Y^>tbDLW>n)UIN++c+w znXadVyv5Km)a)%cy?WX{->hH1I5y%lhzhbT*aJt+V5mQma3E6_vi{Q+5_W`)i3s%o zDVI+FO9K2p?R7MteZ`e zvR5#&4v;yn?|S%6d&;Vt&FT7B6j6ysQ3p{(48~9>&!7=^gwkvj#pAAs>WL^)>?XJ6 ifRe=+ZtD;`j<$N{G#0H9?CeR@hLL^G$Jl1%pJ z#h^Pu2)K3ULU17kcOqgDe}JnXZl$hWxzUrULQS}bobPt%f+1C^}EvVK|;X_}@b+8xLkc>{nA28fnHEwMw}oo#zSmJ>NP6rg9=-S9MM zmZFsa2lx@THIZNj?F}$Sg0SHLvBR*PE_8N;_Q9CE$b;$?gI-59oh0nI!!wCYkLxm{)KDpdet>s z=7ML8YQ}-<9_S30FEm=q4sCi(cig$_ntZL}-!=YmM>~@B`zALH%WbSRTNaI-MkvZz zv~f_g^IkuLaV`9rf2zpta?A3*M8yix;8sbNNVr1!NJY^kJ)x08XqH~!524)6@NT%={UpVp1s{on9pdtZ T^qiXD@t;^5=6vT$c?$mlte^!U delta 835 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 diff --git a/common_functions.py b/common_functions.py new file mode 100644 index 0000000..fad115e --- /dev/null +++ b/common_functions.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +""" +/*************************************************************************** + ScriptsLPO : common_functions.py + ------------------- + Date : 2020-04-16 + Copyright : (C) 2020 by Elsa Guilley (LPO AuRA) + Email : lpo-aura@lpo.fr + ***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ +""" + +__author__ = 'Elsa Guilley (LPO AuRA)' +__date__ = '2020-04-16' +__copyright__ = '(C) 2020 by Elsa Guilley (LPO AuRA)' + +# This will get replaced with a git SHA1 when you do a git archive +__revision__ = '$Format:%H$' + +def simplifyName(string): + translation_table = str.maketrans( + 'àâäéèêëîïôöùûüŷÿç~- ', + 'aaaeeeeiioouuuyyc___', + "2&'([{|}])`^\/@+-=*°$£%§#.?!;:<>" + ) + return string.lower().translate(translation_table) diff --git a/extract_data.py b/extract_data.py index 8de968c..5672604 100644 --- a/extract_data.py +++ b/extract_data.py @@ -119,19 +119,24 @@ def processAlgorithm(self, parameters, context, feedback): study_area = self.parameterAsVectorLayer(parameters, self.STUDY_AREA, context) # Check if the study area is a polygon layer if QgsWkbTypes.displayString(study_area.wkbType()) not in ['Polygon', 'MultiPolygon']: - iface.messageBar().pushMessage("Erreur", "La zone d'étude fournie n'est pas valide ! Veuillez sélectionner une couche vecteur de type polygone.", level=Qgis.Critical, duration=10) - raise QgsProcessingException(self.tr("La zone d'étude fournie n'est pas valide ! Veuillez sélectionner une couche vecteur de type polygone.")) - # Check if the study area is 2154 - if study_area.dataProvider().crs().authid() != 'EPSG:2154': - iface.messageBar().pushMessage("Erreur", "La zone d'étude fournie n'est pas valide ! Veuillez sélectionner une couche vecteur ayant un EPSG:2154.", level=Qgis.Critical, duration=10) - raise QgsProcessingException(self.tr("La zone d'étude fournie n'est pas valide ! Veuillez sélectionner une couche vecteur ayant un EPSG:2154.")) + iface.messageBar().pushMessage("Erreur", "La zone d'étude fournie n'est pas valide ! Veuillez sélectionner une couche vecteur de type POLYGONE.", level=Qgis.Critical, duration=10) + raise QgsProcessingException(self.tr("La zone d'étude fournie n'est pas valide ! Veuillez sélectionner une couche vecteur de type POLYGONE.")) + # Retrieve the CRS + crs = study_area.dataProvider().crs().authid().split(':')[1] + #feedback.pushInfo('SRC : {}'.format(crs)) # Retrieve the potential features selection if len(study_area.selectedFeatures()) > 0: selection = study_area.selectedFeatures() # Get only the selected features else: selection = study_area.getFeatures() # If there is no feature selected, get all of them + # Initialization of the "where" clause of the SQL query, aiming to retrieve the output PostGIS layer - where = "" + where = "and (" + # Format the geometry of src_lpodatas.observations if different from the study area + if crs == '2154': + geom = "geom" + else: + geom = "st_transform(geom, {})".format(crs) # For each entity in the study area... for feature in selection: # Retrieve the geometry @@ -140,19 +145,26 @@ def processAlgorithm(self, parameters, context, feedback): geomSingleType = QgsWkbTypes.isSingleType(area.wkbType()) # Increment the "where" clause if geomSingleType: - where = where + "st_within(geom, ST_PolygonFromText('{}', 2154)) or ".format(area.asWkt()) + where = where + "st_within({}, ST_PolygonFromText('{}', {})) or ".format(geom, area.asWkt(), crs) else: - where = where + "st_within(geom, ST_MPolyFromText('{}', 2154)) or ".format(area.asWkt()) + where = where + "st_within({}, ST_MPolyFromText('{}', {})) or ".format(geom, area.asWkt(), crs) # Remove the last "or" in the "where" clause which is useless - where = where[:len(where)-4] + where = where[:len(where)-4] + ")" #feedback.pushInfo('Clause where : {}'.format(where)) + # Define the SQL query + query = """(select * + from src_lpodatas.observations + where is_valid {})""".format(where) + #feedback.pushInfo('Requête : {}'.format(query)) + # Retrieve the data base connection name connection = self.parameterAsString(parameters, self.DATABASE, context) # Retrieve the output PostGIS layer # URI --> Configures connection to database and the SQL query uri = postgis.uri_from_name(connection) - uri.setDataSource("src_lpodatas", "observations", "geom", where) + #uri.setDataSource("src_lpodatas", "observations", "geom", "is valid {}".format(where)) + uri.setDataSource("", query, "geom", "", "id_observations") layer_obs = QgsVectorLayer(uri.uri(), "Données d'observations {}".format(study_area.name()), "postgres") # Check if the PostGIS layer is valid @@ -160,8 +172,8 @@ def processAlgorithm(self, parameters, context, feedback): raise QgsProcessingException(self.tr("""La couche PostGIS chargée n'est pas valide ! Checkez les logs de PostGIS pour visualiser les messages d'erreur.""")) else: - feedback.pushInfo('La couche PostGIS demandée est valide, la requête SQL a été exécutée avec succès !') - + feedback.pushInfo('La couche PostGIS demandée est valide, la requête SQL a été exécutée avec succès !') + # Load the PostGIS layer root = context.project().layerTreeRoot() plugin_lpo_group = root.findGroup('Résultats plugin LPO') diff --git a/summary_table.py b/summary_table.py index a970b89..58e7ee7 100644 --- a/summary_table.py +++ b/summary_table.py @@ -35,13 +35,16 @@ QgsProcessingParameterString, QgsProcessingParameterVectorLayer, QgsProcessingOutputVectorLayer, + QgsProcessingParameterBoolean, QgsDataSourceUri, QgsVectorLayer, QgsWkbTypes, QgsProcessingContext, + Qgis, QgsProcessingException) from qgis.utils import iface from processing.tools import postgis +from .common_functions import simplifyName import processing @@ -56,8 +59,10 @@ class SummaryTable(QgsProcessingAlgorithm): # Constants used to refer to parameters and outputs DATABASE = 'DATABASE' - ZONE_ETUDE = 'ZONE_ETUDE' + STUDY_AREA = 'STUDY_AREA' + ADD_TABLE = 'ADD_TABLE' OUTPUT = 'OUTPUT' + OUTPUT_NAME = 'OUTPUT_NAME' def name(self): return 'SummaryTable' @@ -95,12 +100,21 @@ def initAlgorithm(self, config=None): # Input vector layer = study area self.addParameter( QgsProcessingParameterVectorLayer( - self.ZONE_ETUDE, + self.STUDY_AREA, self.tr("Zone d'étude"), [QgsProcessing.TypeVectorAnyGeometry] ) ) + # Boolean : True = add the summary table in the DB ; False = don't + self.addParameter( + QgsProcessingParameterBoolean( + self.ADD_TABLE, + self.tr("Enregistrer les données en sortie dans une nouvelle table PostgreSQL"), + False + ) + ) + # Output PostGIS layer self.addOutput( QgsProcessingOutputVectorLayer( @@ -110,68 +124,110 @@ def initAlgorithm(self, config=None): ) ) + # Output PostGIS layer name + self.addParameter( + QgsProcessingParameterString( + self.OUTPUT_NAME, + self.tr("Nom de la couche en sortie"), + self.tr("tableau_synthese") + ) + ) + def processAlgorithm(self, parameters, context, feedback): """ Here is where the processing itself takes place. """ + # Retrieve the output PostGIS layer name + layer_name = self.parameterAsString(parameters, self.OUTPUT_NAME, context) + format_name = simplifyName(layer_name) + feedback.pushInfo('Nom formaté : {}'.format(format_name)) + # Retrieve the input vector layer = study area - zone_etude = self.parameterAsVectorLayer(parameters, self.ZONE_ETUDE, context) - # Define the name of the PostGIS summary table which will be created in the DB - table_name = "summary_table_{}".format(zone_etude.name()) + study_area = self.parameterAsVectorLayer(parameters, self.STUDY_AREA, context) + # Check if the study area is a polygon layer + if QgsWkbTypes.displayString(study_area.wkbType()) not in ['Polygon', 'MultiPolygon']: + iface.messageBar().pushMessage("Erreur", "La zone d'étude fournie n'est pas valide ! Veuillez sélectionner une couche vecteur de type POLYGONE.", level=Qgis.Critical, duration=10) + raise QgsProcessingException(self.tr("La zone d'étude fournie n'est pas valide ! Veuillez sélectionner une couche vecteur de type POLYGONE.")) + # Retrieve the CRS + crs = study_area.dataProvider().crs().authid().split(':')[1] + # Retrieve the potential features selection + if len(study_area.selectedFeatures()) > 0: + selection = study_area.selectedFeatures() # Get only the selected features + else: + selection = study_area.getFeatures() # If there is no feature selected, get all of them # Define the name of the output PostGIS layer (summary table) which will be loaded in the QGis project - layer_name = "Tableau synthèse {}".format(zone_etude.name()) + layer_name = "Tableau synthèse {}".format(study_area.name()) - # Initialization of the "where" clause of the SQL query, aiming to create the summary table in the DB + # Initialization of the "where" clause of the SQL query, aiming to retrieve the data for the summary table where = "and (" + # Format the geometry of src_lpodatas.observations if different from the study area + if crs == '2154': + geom = "geom" + else: + geom = "st_transform(geom, {})".format(crs) # For each entity in the study area... - for feature in zone_etude.getFeatures(): + for feature in selection: # Retrieve the geometry area = feature.geometry() # QgsGeometry object # Retrieve the geometry type (single or multiple) geomSingleType = QgsWkbTypes.isSingleType(area.wkbType()) # Increment the "where" clause if geomSingleType: - where = where + "st_within(geom, ST_PolygonFromText('{}', 2154)) or ".format(area.asWkt()) + where = where + "st_within({}, ST_PolygonFromText('{}', {})) or ".format(geom, area.asWkt(), crs) else: - where = where + "st_within(geom, ST_MPolyFromText('{}', 2154)) or ".format(area.asWkt()) + where = where + "st_within({}, ST_MPolyFromText('{}', {})) or ".format(geom, area.asWkt(), crs) # Remove the last "or" in the "where" clause which is useless where = where[:len(where)-4] + ")" #feedback.pushInfo('Clause where : {}'.format(where)) - - # Define the SQL queries - queries = [ - "drop table if exists {}".format(table_name), - """create table {} as ( - select row_number() OVER () AS id, source_id_sp, nom_sci, nom_vern, - count(*) as nb_observations, count(distinct(observateur)) as nb_observateurs, max(date_an) as derniere_observation - from src_lpodatas.observations - where is_valid {} - group by source_id_sp, nom_sci, nom_vern - order by source_id_sp)""".format(table_name, where), - "alter table {} add primary key (id)".format(table_name) - ] - + # Retrieve the data base connection name connection = self.parameterAsString(parameters, self.DATABASE, context) - # Execute the SQL queries - for query in queries: - processing.run( - 'qgis:postgisexecutesql', - { - 'DATABASE': connection, - 'SQL': query - }, - is_child_algorithm=True, - context=context, - feedback=feedback - ) - feedback.pushInfo('Requête SQL exécutée avec succès !') - - # Retrieve the output PostGIS layer (summary table) which has just been created - # URI --> Configures connection to database and the SQL query + # URI --> Configures connection to database and the SQL query uri = postgis.uri_from_name(connection) - uri.setDataSource(None, table_name, None, "", "id") + # Retrieve the boolean + add_table = self.parameterAsBool(parameters, self.ADD_TABLE, context) + if add_table: + # Define the name of the PostGIS summary table which will be created in the DB + table_name = "summary_table_{}".format(study_area.name()) + # Define the SQL queries + queries = [ + "drop table if exists {}".format(table_name), + """create table {} as ( + select row_number() OVER () AS id, source_id_sp, nom_sci, nom_vern, + count(*) as nb_observations, count(distinct(observateur)) as nb_observateurs, max(date_an) as derniere_observation + from src_lpodatas.observations + where is_valid {} + group by source_id_sp, nom_sci, nom_vern + order by source_id_sp)""".format(table_name, where), + "alter table {} add primary key (id)".format(table_name) + ] + # Execute the SQL queries + for query in queries: + processing.run( + 'qgis:postgisexecutesql', + { + 'DATABASE': connection, + 'SQL': query + }, + is_child_algorithm=True, + context=context, + feedback=feedback + ) + feedback.pushInfo('Requête SQL exécutée avec succès !') + # Format the URI + uri.setDataSource(None, table_name, None, "", "id") + else: + # Define the SQL queries + query = """(select row_number() OVER () AS id, source_id_sp, nom_sci, nom_vern, + count(*) as nb_observations, count(distinct(observateur)) as nb_observateurs, max(date_an) as derniere_observation + from src_lpodatas.observations + where is_valid {} + group by source_id_sp, nom_sci, nom_vern + order by source_id_sp)""".format(where) + # Format the URI + uri.setDataSource("", query, None, "", "id") + # Retrieve the output PostGIS layer (summary table) which has just been created layer_summary = QgsVectorLayer(uri.uri(), layer_name, "postgres") # Check if the PostGIS layer is valid From c44e3b9555596c31ba2f2f87a8386de7da4c9e2a Mon Sep 17 00:00:00 2001 From: eguilley Date: Mon, 4 May 2020 19:58:18 +0200 Subject: [PATCH 04/19] Ajout d'un script contenant les fonctions communes --- __pycache__/common_functions.cpython-36.pyc | Bin 1655 -> 4339 bytes __pycache__/extract_data.cpython-36.pyc | Bin 6067 -> 4811 bytes __pycache__/histogram.cpython-36.pyc | Bin 5944 -> 6053 bytes __pycache__/summary_table.cpython-36.pyc | Bin 7678 -> 6328 bytes common_functions.py | 102 +++++++++++++++++++- extract_data.py | 87 +++-------------- histogram.py | 11 ++- summary_table.py | 97 ++++--------------- 8 files changed, 143 insertions(+), 154 deletions(-) diff --git a/__pycache__/common_functions.cpython-36.pyc b/__pycache__/common_functions.cpython-36.pyc index a448e50a2efc3eed11a265cab87e2f183d56be04..f39ba4a8703ef400c438159e59b9de37e21b767e 100644 GIT binary patch literal 4339 zcmb_f-EZ606(=c5qGj1}9Oq+Ok=-mGj>_SipQz#fKeKGL<^4E3 zy!V{*`<v;5J`Q943PztA?zbd;9y9HV1&9M5rjgihdjgdU}n zcuvq`bPCU-^f*0%=One(waQ87-#JYyRWH4|t3>+Oy34(0Bv#h$l6m5WjYb&QwK#Ai zFAT(Nvo#c)dG;Ev-F6~2P&jq2xl?Y;mu?9Sz0MnI{()DoUDc zwaB6`9I_mHzRz0ZqTJZ6`2MZRP{m@y@%+QD%lHvo&5Kug+MPs-;(K z{7X;o|NkCe!a?!!# zfL23J8X@&+EkgRrun-57aT0Aa5;5KoB&?~y^6HPsG7A`Ye6kjA`JPKwJeLK65eMIw zGh&-j>BfOFsbfj*X1%`~vJ^tQBLnO@VII~wdC0iHn9L16)Yp2MV0ZHB@9d%z$+K`0 zHs$*j9H2#fMBh-wtP+25(^vLlp_g5i%3j@7EwxYn4*;}F|HV`L>`Ne~J$h~_JmyX`6i}!c8Hd;+4(i0f0@z7;LctL$}-(||asd=yNiB(sZ zFXNJQXyRVLEnY(tX*)UvA<|qg*VQ}Pj?vS0avi;=gSO1}H3iyruhH~nd^vDc3$6;B z1|yHS7ey?Pp&~;h7L3m-@%TIxC%;L4oBTdGNIpwGPyUenG5I3-Q}XBJugTw%zyI?u z$v=`$e>OwXDaUabF3$_YIF4H_*S78WEZ(2HFn#gEk2~E5m7jj}lj_ay&b)o;@t5Zw z|L*+bPtVT&;LP>;w{BF-GzToCBMoPlMcfHQn)ky;3^QsZ!>VGW1&NoSo*o@cBJS+S z+49+e#>eofuWHrpu)(UVo^e)n)(>6BuZnFzuX=C!-F4Mmtpc7k&u5}aS5r z+hI^``f=S0L{(%|Y=gk6&tc+pO!N(DO9M(Pi4q#kG<5^Ng29hqG<249S^>9m)*I+Z zXJszx>D1aS^0_FtW5Qv1id+^Ic8W5aePZfbWKpxH|6HdY^D0=!Aw)F3vuY_JU9IK6}Xg)S}22^8M9{!6&f9kIMBCp2H&@164M0j`hs{#YY)f zxCB93Y5*!uoxS7mbhH_Z?K|0+=jE!kfZ+BJ1YDv;qE6XEg-h0qOR*_z*4?$$nq6lI zqKLPyU`1GHG;>lvrCU0eKy&3+T4T5<<6GP0<+jCezC+l!OWk=NGmtsoK+NXY?V z`^kYDqlW~AuplCK-Q+Vt&a8q0i=KE-MwRkXYDqV~8`MGsh*e{ZZaMBQ{}wj!Lce*! zR-5=f1p9cjfJQ4<(mBE05RYH;+y$&e8a$=>eioRSKe?m>oH^&SAe|VzzSMs^9j&uyFaQ|jsnd~6 z3ix>10A;}RKc)TB_=ZLcCn2v) zSZx0|g8*vUh{tY}*>KM%4(Cn_F&;#Y7l7u~!m~Dso`Xm_(!HO=H}!aGUy+al$yIbID#qu99w-RI0dY2_3{WxJo%&I^B?r z)rHH&1&neiBlMnuaVBMU^g3MKGjt6Bke99nqnSWNzMa{AC3Gl|uK1I9f%2UjgFOPI z3?A+PA$>ACj*s6@4g~U#6QNL58JFO{g@VP>YNe1C`){OUO5X-b*85=?r6V;jpvydr zo9QUBG~-b=C_9wiahfZ#q7kWuk?NwVD9Uhx`!h$FTxyxfJ}Gm%U6;z9hE&PNVX{=f zuvt+?qnTyBq?>wKpVaw0#;&xeyd4BxqbgaCkHT{?wVOi)-gIJHGYl4tfQdu zNnY3X3KAx;dR|$w0819z(+U`i`wT3Je+=Lk)b@hF>truxARN(;^Ame01?QVc19-Ab z9#WDlZ{OOu_0FyJ#WV-%-2llb~eh=o9$k4|7Xn8-^^U zWIRhSt{ZcX}Q@r z@_dmM4)T%d5JLO^Zk(6JP7RW)I~g}-sJ&u+-G csOsDVF7eBmM%kFkSvhOe8nMb~k6TCo1FSNH&j0`b delta 197 zcmeyY_?^eYn3tDp%A@shcUT!19y1^TCLr4Zh>I0~L<&O`LkeRQV+vCgQwnnwa|%lo zOA2cWTMI)JYYKZXgC@tu1Tac5PR_T|Rn<@)3 zj0;FGFtRW*0U;Yx5s2%j$u?P0z{U*BDdGT%AQ7BE?kx_R-29Z%oK!mzAP;N=3lj^_ J5Ef<@CIBZLBP9R; diff --git a/__pycache__/extract_data.cpython-36.pyc b/__pycache__/extract_data.cpython-36.pyc index 6004f085fb9f5aa4c89b017887cc95fc77638a05..d08993105276ca4f03121981f67558bbc107f1a4 100644 GIT binary patch delta 1370 zcmaJ=Pj4GV6yMoh+w1MxS+};6IBrOqRCsA=B5|lHP}PW3Q2?c)Jz*hM%g)$YvwxhK zaZB0_J}k$o{27UF00<5UDImlbKtkLR+G|h!0vr)<)@`7ubVu{{&6_uGe($~C9B=$~ ze(jZ7&HU=-_wW7KRFsFxzdjxGYcSalpRFC-KngBlD{M#e{dv%9xE?M1~~8fNz6E%_}pa& zq3coV9lGNrJRE{OH^aokvp_z?KKTI2EW^RETfUB3XJGw}0#I~D!U3I!C7l;PS0ePP zc+uGJNrQvvLSa7M{I?wXb1Hic@aCL9}Ik753noP#Kcw78l zef!*V97~1c&hf7K3qnm{TRqegSFBB=ErDuuK^$1^NLb(jm6EDEXbS`4G?*GkkH8Ed1LHRXdx$bi{0Whh=&y3g+goM&(jx| zI#RWzI4y-NbFK#1F`?8C&Rm-2_5U!hbZ4JXlAF->nD`P3y&?})XRHQ@k69+e745Od zH1P>zCmo7mi^-dmvaK4Lp_-bln#k5VstMz6_xgvNdOqLwxcIL5?yBTnUWUgYu|M5= z+Jn9O;zH}%1xfY)p>o})#N%Wa9+VgRz!#)gra5=3M~q;B(+s3)}%Nu#fB7; zM#WupM{56oC^VTSmG^JLWHOSW>za;W$ow7;bKO`rw8GChN70)iX1 zf($S5BncUIQnOiqi+;%_&ziO@T`M@CG4we%Tz3$J#C7S4^pLQg mg*P`|ixNBu$1u&l1JuZpL$%O)#X<|JrTq>4lS1$S delta 2679 zcmaJ@&2QVt73WZtL{Z<4Bg-GzjFW6_wW*zUQ?w`+i)0hW$<|Js*v%qBw*sRX$)qV# zo+16AP*qX2KraPa5a^+QKv8G6ha&d{3Kazwi{5&g-R-5P{0lkt4dti_nyv*tbKbl+ z^Ly|8IB!0G``PNnck+4TaP;S&{_DJ={9AeDPlNYs@bFK5cX9t(r&6zUX6iGYYQ5T- zt&(~ZkwTO9f_;v@HAMCE>k7$}!Y>t4prv7=j!9};sTKe7WnV+tsGfYf zA+q<|{KLJ+JI7th#WW1InAfD7TW7ux-{#YN?{xwWGv?fRDI){sbdJ??|a zGbi;Ebao10o+zN!ICWaA0z+1bzDjq{d(n^d-DRn$AvP21C!z{(B11QLbnFwxh1)lP((tqqkSA`5$IcH2+dsi9)0NrGs;ZfBsbso!Ww@eT0G z5^acv=obila$uE*rvOipq`_aGRr*Q41aswhz06=wW8rw%Pq{k0O%0WPiWvQLm?YUf zmHnRN`nqe7ynQB24pCn}+6hxPm5(<1nJ{BdgS7z=&xXbrVm}*Z*_UCO6u@2wZDpu{ zUt&h-r}zH|`r>Q)!Rf?@3QUy%nM7HxD!)=^6*48A$Qf_|Z9fOm&BU3h#+k|cv-Vt= zhnYfHu;Cfd<`IB)p5dH zBTU$s%nlJ!Le*Y`q+NjMN@2;qNakRha##kpr!c-mGN4gGGq!s>EVdDuAFAM4w!aY; z<24j}2{v0EDi(qd>Rj)A1E2ogri@~X*Xl5#uyD) z&<1X7n7HS;6qBXVi60P(TVB9ii(+?)avyh1jt@-7f{8`^kOr3H&>rTa6NfhC7wA%k z18{G8L35Yl1JJ_C7|i|SL6_pKdmE3|@7-Nnd2YaFHB}@XdO#iEa==Wvzzg)l=fF;d zNzqH1F2Tz}`|fwXcU5R@>UDY*?l*j9y1eDF&azZp#!vdS-YV!1f$x^P{G@*wZ|^it z!@I@2&JG+M%Y&sS{iWErhCPOR-~JzuP3bbRf8G4XTS?zl9KIGHj4p@^bI8n6099~5eX;ODR{ zROK|ITX4Ls-`U2^U6Zv(CvU3DYpCR&H+E@r56a|FjvcSfWjS81(}nWl1B(YxR6rZs zbl}J}+h9zV=pd%XO7Q57Cjd?gyi%KE>PjH?WC;OQRD^k47iW z0NyAzff<+w&F1J&94`j)czZB9;lN7M=eX+xZOg?QTla1aYARcWS#||PttKk*iAR0P zuJeab1dnZ)$CR;YX_E&b@}~#)w$Gq%i1gvOp|FIUG)`lC-((`&4fyWnI967av-%A< zF|DTQu##ME3B*9;WHF%;9RTT>NHiH2DKh|~5s`!rVfiBAP*;@3q@d&$HT{4=1IxCl zKN+MXT}@x;lCjih$0BF(Z2;&{+3rNrgwE%S@+;xR0qAXZMq(*PpB63;W+lqB#UIK~ zQ0Ov%J8sK^uE#tZvLW)&u_)VNls@o0Uu0UAOV(xc5n0QH`ugKR&Lm{h?8d3%LWj{f zi?R+vwMcDh+-i&o94^vLFRB*j2W(9mWyayc7b}u6nb{9wY^`zhLNggPML|aUYLxo{ zh0qP?U@pJ~Z}uQwEru=0FEl2Bm&O#3-%=y)iIW~TIBjwz2wC0@CVg%n7UH)+D5;V< zg{r8a&Lv97Q1yhS>L{fe@YB_Ts-dbnr&d#jR!SIPIhQa{3hazT4#~G1nnH%&TUa}S zbEfIvgz~TaceCiz;?|pHrO}9z^ z7X6|0=*MxPVO=qTA*3xh*{r zk7|pIMZEldv=Dt<{@Eb@U|2!gO8&>JufI$zaC61?JcqNgoI3|Xq>05`xDW}w(P&u? sZ8R9%4#zva34+ky?RX?`=zB6OE`!k^|L~JFDMG5@r{>VrEXc%v0M73H8UO$Q diff --git a/__pycache__/histogram.cpython-36.pyc b/__pycache__/histogram.cpython-36.pyc index c1bf1603b0e14f0e1e6246aac45d033024ceca46..e6ade962384199f254feb9bec4dd95905ae39d76 100644 GIT binary patch delta 1490 zcmaJ>&2Jk;6rWk|+UxZ?-uNq@&38l6iV$c62ddCOXbB}%MWF>tbme-+&Tj3snH`Hp ztQEutB#=NtBeftvsE0~$KosS|U%;WjKd=Wb^n%2h11H|BlTt;c9{IO#-~8sy@4b1m zKP}yUV)jHfYusDDbLI09%#-9rv}pbK`z7tzpBO?fDp9ev zMQhjWW_6n~yFuG6%Gq{g`jp#sTIF3v_f8}iv2pRW*Vo^)*Is>bP1IAzN(ET`9a_YZ z>eFVO4Z$lL61P(>UK3BI%VJ)Cc0oE9F&mEO?!yRJQHD5v2lhN)ZP9h{k^brE!&s_Q zYkOxQ-b-_jh?Q!vcDJ)ZUKU@c&&@o7sKvz1J;S(xaZ%jLEZ~y3o0(aWdq+vwtL`)+ zom{m$RaR~7Cnbf)nzN4Q@U>>6%^befx*(>_&7F^-iUD#_fB|u!go&QYl|T(K91-qm z*Dvsdd|O9vt@n~a(p5v$OL5Ij1}Qfcq7e7=Kxc0R8c|#wd}-+$LOt#6dGI9;`z|B! zs@w>)5EE^92YsN7Ad-{|ND9c{?xjIWCKhDIf()K@2Lc0TGC{`85j{u~mFOfLY8Y_~ ze1jn(2FZYUGcetJi2QrJ08xs>3~`9x$DR=++!D#kw0PMa3K66Sowc)zV8L*R)@3Hy zm}G$rAjTCKl+E`5V<2Yafici$K(G<0NG`~^qoe?%NQw=pSI-J8cPz*PPr(T6aTznV zj*!y6H9^wleMOUde`UYGxUlzO&cY|3I%qcNZ)IsYO6_;@p%eYbIMsA~UTe2Ht6z${ z)>jiJ<-(PN)qT1=d-gQIH)3~iMq!rtc5riHDk{fKLOEq?{v}%DZT5=1L#!nEgA(!p zVk&fbIIGHyhx!`e0K`}ceC8cg}80Z`IXbn#2!)^xw6ea;-OaVTxJ0L1UbC&ks{ z@)rNk4IQX~7*01lpI5yay(9iEUfwAJzn@@PTJXg}Y)rzmgjoqkB`is}B0=uOu1feX ziP(zED)*PA^<^YoRTCj@U?e2n3`_^tE|J|eX8jbRJUt=0DIde^IQ zr|tRsMHe^A$FHA(c#-CHTb=Dl@j7fmzNd)8Jow;5x^35;Cbexg1^zht!{kN!saBhG gn{-v??Mtqz#0nFj5u1vMmkkq}7>bS!tSe^XUloBM*p_!B zvt`I+RNd`hoKeQ;O)q-U!WhFa_5%iM|G^Ns80=~cHg>sQ^*wUhjSwoK=e&B)bDsA( z(t92r`RVZdaa~u(4+C`NwY&l9 zkI)hhMc3+X&>Xzd9Q!S9;}v#Zd4G{7mN3mn`Ik_DC`^)$JLony*P1=+UG}r`WqB`_ z*l9WaGpw6OG7zY&*G#vrN*eCwc8qhFTk) zcBqh#)(1`6?Corb8;?BeoqoqV-EBLxn3*i|x+cK#&!^ zgf9h{NK@zwp@fKBWXrm|73axO0t~2;jbvm)^)y@eRhUWoNjpUpKS4yIkVGJ1c{LH(E|)C@^&FW7?Bi7-$lTWHV5kZRzxzu zF%UVGWOh+f5hDwX)Pw zb_~sOL!I%7)Yljr{>Nq}Hx~`Aj|4?YLgc=?!Y%Fg za+EJT_G0yf&i^_+4sf6SlBo)RVD@L`;-N}dh}y&QEPC3#ZnZpzUTAJwlxBe%Cfk;G zW>VB;x3X){Xy0ew#s}En**P|Cp2d0AG8fFxoW8Ygc|&4_(kHhqaCnS;YrcWsVNcA3 z*C$7bmybHn7*MBWx$ev6vti1u0U+66<=lh)lMZ%0+Ud7N{nr>!V4 z8r5H*gv}J*#ASB1aF5BwgPA&zLa9IO4K_oeKcLgBRxGZ~a!tNsFX2!*)pN+OYt?uy bm%9@M5yKr51qEw@hL=?hMg^-_5wzHIrer9P diff --git a/__pycache__/summary_table.cpython-36.pyc b/__pycache__/summary_table.cpython-36.pyc index 0cddf0ab1e1142125652a6a1288503876ee97169..0648d16264ae63a181e8afe287d10c8c0b4ae3af 100644 GIT binary patch delta 1655 zcmaJ>TW=dh6yDk0*z5JK?Ig~{jtgln5Ti6uK&^mKT2T;{P>PgtvjnTn&cs=FFV5^N zX_Ad1VF{H-G(zHyH!g3W@PLE_67T|uAHag(fhYa~4~R2knuZE$Yd)VjbA0AImpOku z@%h>LM{L{t;l)p0|LI;u`CU2j(LuflEBWl5`PUvt3Le2`Fdo)Bbt#vFiEy$r39^Ol zU@DyMOoxq5BW!k>;Y?>HY;{`Usm>{+eEQ{EIx_e(Bj53>h2~))w-Z0?2ma=+6S*PD z>)saet~-IdOQ_Q&aYz{5{kPUnoHyLS$9c<(qlD4aV@|RiI4-5`uG5c$-7e^JGl*S0 zbd-;i8^lW)k#^fDp*~4Gh~_9O?4yYZkUm!0DAyAbY|E@uGvdyiZ9!XFHH+pB^TqXDeI?<8iH>_Kog*?$tdyy@Y(dcxq&yFzDwQk zpP+@EjLY^TpIr*NG4vjR&oLHsmkhMRu~{Cy4usTLeCf|19YIX zlF%L$u?E^ANJggglneoynL5x8O03+ovJ#w_ptI#kMV^$iGW`YXC%jpB1a{T{;eze*R0!m|l;b|9^Ov?LmQS?trukE5migErqDJA>Xk zdVlOZ;=hf*U0TbPT=lWbj{c~R3l+XEH~MkHx|AgCW!LD5!c9VI`A<2u-S&ZCZxv@zI-f+F^JZvm3=9N2M=g7_$p(HoKJR%;0=;>4Z z=f-25J3Fz~r?E$pmQ>22*Lwu)ox^m8V&d}t( zJx^cZAGFS+)BM+#cbJ>oT|Ze#nI9ywPMup>gD6*69D~{hH+jvA!!VAV%`}oZPiR$K zAv;7?T6Tymw`dDGC;n8#K9#fCaGXs)AdW*9#GxQ28Lyt;EvJXE8pb#c$OYjf5raop Wbp&5cR}EyT7FseaWT9Er(*6cdPMGNc delta 2983 zcmaJDO>Y~=b!WLGm&+wZ@kZf1P4};5NpneT57pV z&&=AAyadw{?Ja#RG`scqC?ZQ`7JqBFTM8RJv#$&Hi0`@>H(WJZ^hf4VHPw_EJ3 z?fdJyUBW~eHmlTc62=^_b;b8M+2KOF-Ex?yeuj2!r%Afp@x91M)kcsonbYaI&gSm2 z-62nZnd+lT8LZt`>PTpexSO;FTUv`Q>R$A>OfCAOzBngQbwp3Zl;>a)Rw9cX)^+Vz z-SucH^RwEQacNG|_ijdi&wT&P*XT;Os2_B1;-%=j#`5W}VNAx1D^EW%_Ec1f&gJG% zHCoHf+#Qo9Wo(RB0^SWc)uplH5=;Q7Ct?>w3XIuwT0+P78(o{)okO1`U+h%Q@f?2H zZTZyUTb(=6Uvu_|R4vtAfsg%4jTZBl`fWW-wKD@{fbiMvH0^SuogFA`6KgohbL~7% z;q-eNQus)yv!9M^$`epdH6rdEr(98h!acNxnu{xpPvJ?Uz zl@I}(4AnuRm**$jr@}m#BE5ehrdTnh5EkI*|KQ3oUq*2icB2r9G5HJXRXje#v=Y7I z&dXsDoH^Gk@u~J{JQ3!@67JW2q2d#v8Yb}Mdr72(iFN{?93Z4TK@UCzOGnz%xEPM& zB0hy*7yySii7ikHw)aXg^%SUiPUU9+Hv>`7@Dw;u4$JMc1LcXzUOG(R>0tsrBR*-` zl}CkYdls@d2LY~xm3AGU0p-=O3S#$Re-0afQ2{gL`$kx9Av`lsL9*JO56e;q)Xn+l ztA*hTV)h~!@X|nW(3(=8jouyE=!^8K@9wsIPnfp?mpezh)HO;;K!X+(=aOyh`5v+G z?7=<{FtIlMfO-zGyjjAy)wP-R&~_alOj&nH;J7Z?v)IADOPaDzc!XL3h&TP9xkapp zfCbGKu=CvuU1F`SEZw`YvV3jf69W#bt0L)=hs1@;6i{0#c!pl5MIccGAQB_Be$w`^ zH7B%}FTDDS&|1Xr>{S_WaB6#O)2E#|iJG?__v(8u0{#(PI>(!P+?%)7)*FW|UZ;L% z9h%(S?Bm{S%v-m7YVDo>g1{{)ah$z$jDPtP*`J@G=v3i6`t{SL!fQx(SR)QdG+QFk zzei{Kd({WfNMG;zjKeS4AQ|uKJ5AfABT^mAbw!xi<~9u4 zhj3k}NHi%EDLa7j#3Bh}&*38B67PGW0FzT*6nvf7JfJY4vn|4pHuPkyUsgHcbXS-T zTLTYVB8Rz1+A!C7Tv>E9dR98qkEfW?eLmksf(D+*F~YBcIct!*CZSoWGZ9cnq#brw zUTBd9kv5niaxjnyU8jWH@qI2bn~sNX$k{5gjtA-H!;J}p@|N9QlFUr#u#1<`N_pmN zJT$Mu8TGN@&@|#nDKo(6Wx&%n0Ytwk&)lS|(v-|F1keCP^3fKdgkF;9^yV-kLbEBc zMP3HsnDYi)7jM{j=sVGW%irz`jY^vf!(wQ-UB3}e`_Iql!eM-#kLf3H9HRh~Q~~Lz zkSM4{G>-CWO)Vfp)zy4LQ+1S54fuzhnnL418`lc3F91HD(9^~RWF`!xAyriYr6LvS zYC$dQhFVk$KuLirBVi)BGEos3ndse0=d^tEh!h)E=I{jtnD{jjSCWaMk?I@s@~QO| z52<_;;={>Jk8#^;k`WX=uHI|N8$#pqmPPUDQzkAcdP?HXNFaL*os;0A1iKQ*3Z^{? zJ~b#_$u|y_rhGHHI5w4E)I&WH@9Wd_htd79*N_$ce5~1zuN2KmZh1rEdm#+j8y5oZ zxGcU^8%K$NieBKp59m=~7KNtY>G)n_Gw|ZM!e~kA_<;mdk|B*9>cO2BdZV%FxTMjb v(~>9+i!EL}W_SE7(=T;=9JpjrvM`C#R1I>a#fddl6Mf4xAuA@N*G&8$>C!^D diff --git a/common_functions.py b/common_functions.py index fad115e..b13158c 100644 --- a/common_functions.py +++ b/common_functions.py @@ -26,10 +26,110 @@ # This will get replaced with a git SHA1 when you do a git archive __revision__ = '$Format:%H$' -def simplifyName(string): +from qgis.utils import iface +from qgis.gui import QgsMessageBar + +from qgis.core import (QgsWkbTypes, + QgsProcessingException, + Qgis) +import processing + + +def simplify_name(string): + """ + Simplify a layer name written by the user. + """ translation_table = str.maketrans( 'àâäéèêëîïôöùûüŷÿç~- ', 'aaaeeeeiioouuuyyc___', "2&'([{|}])`^\/@+-=*°$£%§#.?!;:<>" ) return string.lower().translate(translation_table) + +def check_layer_geometry(layer): + """ + Check if the input vector layer is a polygon layer. + """ + if QgsWkbTypes.displayString(layer.wkbType()) not in ['Polygon', 'MultiPolygon']: + iface.messageBar().pushMessage("Erreur", "La zone d'étude fournie n'est pas valide ! Veuillez sélectionner une couche vecteur de type POLYGONE.", level=Qgis.Critical, duration=10) + raise QgsProcessingException("La zone d'étude fournie n'est pas valide ! Veuillez sélectionner une couche vecteur de type POLYGONE.") + return None + +def check_layer_is_valid(feedback, layer): + """ + Check if the input vector layer is valid. + """ + if not layer.isValid(): + raise QgsProcessingException(""""La couche PostGIS chargée n'est pas valide ! + Checkez les logs de PostGIS pour visualiser les messages d'erreur.""") + else: + #iface.messageBar().pushMessage("Info", "La couche PostGIS demandée est valide, la requête SQL a été exécutée avec succès !", level=Qgis.Info, duration=10) + feedback.pushInfo("La couche PostGIS demandée est valide, la requête SQL a été exécutée avec succès !") + return None + +def set_features(layer): + """ + Retrieve the selected features of the input vector layer. + """ + if len(layer.selectedFeatures()) > 0: + selection = layer.selectedFeatures() + else: + # If there is no feature selected, get all of them + selection = layer.getFeatures() + return selection + +def construct_sql_array_polygons(layer): + """ + Construct the sql array containing the input vector layer's features geometry. + """ + # Initialization of the sql array containing the study area's features geometry + array_polygons = "array[" + # For each entity in the study area... + for feature in set_features(layer): + # Retrieve the geometry + area = feature.geometry() # QgsGeometry object + # Retrieve the geometry type (single or multiple) + geomSingleType = QgsWkbTypes.isSingleType(area.wkbType()) + # Increment the sql array + if geomSingleType: + array_polygons += "ST_PolygonFromText('{}', 2154), ".format(area.asWkt()) + else: + array_polygons += "ST_MPolyFromText('{}', 2154), ".format(area.asWkt()) + # Remove the last "," in the sql array which is useless, and end the array + array_polygons = array_polygons[:len(array_polygons)-2] + "]" + return array_polygons + +def load_layer(context, layer): + """ + Load a layer in the current project. + """ + root = context.project().layerTreeRoot() + plugin_lpo_group = root.findGroup('Résultats plugin LPO') + if not plugin_lpo_group: + plugin_lpo_group = root.insertGroup(0, 'Résultats plugin LPO') + context.project().addMapLayers([layer], False) + plugin_lpo_group.addLayer(layer) + ### Variant + # context.temporaryLayerStore().addMapLayer(layer) + # context.addLayerToLoadOnCompletion( + # layer.id(), + # QgsProcessingContext.LayerDetails("Données d'observations", context.project(), self.OUTPUT) + # ) + +def execute_sql_queries(context, feedback, connection, queries): + """ + Execute severals sql queries. + """ + for query in queries: + processing.run( + 'qgis:postgisexecutesql', + { + 'DATABASE': connection, + 'SQL': query + }, + is_child_algorithm=True, + context=context, + feedback=feedback + ) + feedback.pushInfo('Requête SQL exécutée avec succès !') + return None \ No newline at end of file diff --git a/extract_data.py b/extract_data.py index 5672604..c8e915a 100644 --- a/extract_data.py +++ b/extract_data.py @@ -28,8 +28,6 @@ import os from qgis.PyQt.QtGui import QIcon -from qgis.utils import iface -from qgis.gui import QgsMessageBar from qgis.PyQt.QtCore import QCoreApplication from qgis.core import (QgsProcessing, @@ -38,12 +36,9 @@ QgsProcessingParameterVectorLayer, QgsProcessingOutputVectorLayer, QgsDataSourceUri, - QgsVectorLayer, - QgsWkbTypes, - QgsProcessingContext, - Qgis, - QgsProcessingException) + QgsVectorLayer) from processing.tools import postgis +from .common_functions import check_layer_geometry, check_layer_is_valid, construct_sql_array_polygons, load_layer pluginPath = os.path.dirname(__file__) @@ -101,7 +96,7 @@ def initAlgorithm(self, config=None): ) ) - # Output PostGIS layer + # Output PostGIS layer = biodiversity data self.addOutput( QgsProcessingOutputVectorLayer( self.OUTPUT, @@ -118,75 +113,25 @@ def processAlgorithm(self, parameters, context, feedback): # Retrieve the input vector layer = study area study_area = self.parameterAsVectorLayer(parameters, self.STUDY_AREA, context) # Check if the study area is a polygon layer - if QgsWkbTypes.displayString(study_area.wkbType()) not in ['Polygon', 'MultiPolygon']: - iface.messageBar().pushMessage("Erreur", "La zone d'étude fournie n'est pas valide ! Veuillez sélectionner une couche vecteur de type POLYGONE.", level=Qgis.Critical, duration=10) - raise QgsProcessingException(self.tr("La zone d'étude fournie n'est pas valide ! Veuillez sélectionner une couche vecteur de type POLYGONE.")) - # Retrieve the CRS - crs = study_area.dataProvider().crs().authid().split(':')[1] - #feedback.pushInfo('SRC : {}'.format(crs)) - # Retrieve the potential features selection - if len(study_area.selectedFeatures()) > 0: - selection = study_area.selectedFeatures() # Get only the selected features - else: - selection = study_area.getFeatures() # If there is no feature selected, get all of them - - # Initialization of the "where" clause of the SQL query, aiming to retrieve the output PostGIS layer - where = "and (" - # Format the geometry of src_lpodatas.observations if different from the study area - if crs == '2154': - geom = "geom" - else: - geom = "st_transform(geom, {})".format(crs) - # For each entity in the study area... - for feature in selection: - # Retrieve the geometry - area = feature.geometry() # QgsGeometry object - # Retrieve the geometry type (single or multiple) - geomSingleType = QgsWkbTypes.isSingleType(area.wkbType()) - # Increment the "where" clause - if geomSingleType: - where = where + "st_within({}, ST_PolygonFromText('{}', {})) or ".format(geom, area.asWkt(), crs) - else: - where = where + "st_within({}, ST_MPolyFromText('{}', {})) or ".format(geom, area.asWkt(), crs) - # Remove the last "or" in the "where" clause which is useless - where = where[:len(where)-4] + ")" - #feedback.pushInfo('Clause where : {}'.format(where)) - - # Define the SQL query - query = """(select * - from src_lpodatas.observations - where is_valid {})""".format(where) - #feedback.pushInfo('Requête : {}'.format(query)) - + check_layer_geometry(study_area) + + # Construct the sql array containing the study area's features geometry + array_polygons = construct_sql_array_polygons(study_area) + # Define the "where" clause of the SQL query, aiming to retrieve the output PostGIS layer = biodiversity data + where = "is_valid and st_within(geom, st_union({}))".format(array_polygons) + # Retrieve the data base connection name connection = self.parameterAsString(parameters, self.DATABASE, context) - # Retrieve the output PostGIS layer - # URI --> Configures connection to database and the SQL query + # URI --> Configures connection to database and the SQL query uri = postgis.uri_from_name(connection) - #uri.setDataSource("src_lpodatas", "observations", "geom", "is valid {}".format(where)) - uri.setDataSource("", query, "geom", "", "id_observations") - layer_obs = QgsVectorLayer(uri.uri(), "Données d'observations {}".format(study_area.name()), "postgres") + uri.setDataSource("src_lpodatas", "observations", "geom", where) + # Retrieve the output PostGIS layer = biodiversity data + layer_obs = QgsVectorLayer(uri.uri(), "Données d'observations {}".format(study_area.name()), "postgres") # Check if the PostGIS layer is valid - if not layer_obs.isValid(): - raise QgsProcessingException(self.tr("""La couche PostGIS chargée n'est pas valide ! - Checkez les logs de PostGIS pour visualiser les messages d'erreur.""")) - else: - feedback.pushInfo('La couche PostGIS demandée est valide, la requête SQL a été exécutée avec succès !') - + check_layer_is_valid(feedback, layer_obs) # Load the PostGIS layer - root = context.project().layerTreeRoot() - plugin_lpo_group = root.findGroup('Résultats plugin LPO') - if not plugin_lpo_group: - plugin_lpo_group = root.insertGroup(0, 'Résultats plugin LPO') - context.project().addMapLayers([layer_obs], False) - plugin_lpo_group.addLayer(layer_obs) - # Variant - # context.temporaryLayerStore().addMapLayer(layer_obs) - # context.addLayerToLoadOnCompletion( - # layer_obs.id(), - # QgsProcessingContext.LayerDetails("Données d'observations", context.project(), self.OUTPUT) - # ) + load_layer(context, layer_obs) return {self.OUTPUT: layer_obs.id()} diff --git a/histogram.py b/histogram.py index 728ad55..65c46d9 100644 --- a/histogram.py +++ b/histogram.py @@ -42,6 +42,7 @@ QgsProcessingException) from qgis.utils import iface from processing.tools import postgis +from .common_functions import check_layer_geometry, set_features import processing import matplotlib.pyplot as plt @@ -58,7 +59,7 @@ class Histogram(QgsProcessingAlgorithm): # Constants used to refer to parameters and outputs DATABASE = 'DATABASE' - ZONE_ETUDE = 'ZONE_ETUDE' + STUDY_AREA = 'STUDY_AREA' OUTPUT = 'OUTPUT' def name(self): @@ -97,7 +98,7 @@ def initAlgorithm(self, config=None): # Input vector layer = study area self.addParameter( QgsProcessingParameterVectorLayer( - self.ZONE_ETUDE, + self.STUDY_AREA, self.tr("Zone d'étude"), [QgsProcessing.TypeVectorAnyGeometry] ) @@ -109,11 +110,13 @@ def processAlgorithm(self, parameters, context, feedback): """ # Retrieve the input vector layer = study area - zone_etude = self.parameterAsVectorLayer(parameters, self.ZONE_ETUDE, context) + study_area = self.parameterAsVectorLayer(parameters, self.STUDY_AREA, context) + # Check if the study area is a polygon layer + check_layer_geometry(study_area) # Initialization of the "where" clause of the SQL query, aiming to retrieve the data for the histogram where = "and (" # For each entity in the study area... - for feature in zone_etude.getFeatures(): + for feature in set_features(study_area): # Retrieve the geometry area = feature.geometry() # QgsGeometry object # Retrieve the geometry type (single or multiple) diff --git a/summary_table.py b/summary_table.py index 58e7ee7..8420332 100644 --- a/summary_table.py +++ b/summary_table.py @@ -37,16 +37,9 @@ QgsProcessingOutputVectorLayer, QgsProcessingParameterBoolean, QgsDataSourceUri, - QgsVectorLayer, - QgsWkbTypes, - QgsProcessingContext, - Qgis, - QgsProcessingException) -from qgis.utils import iface + QgsVectorLayer) from processing.tools import postgis -from .common_functions import simplifyName - -import processing +from .common_functions import simplify_name, check_layer_geometry, check_layer_is_valid, construct_sql_array_polygons, load_layer, execute_sql_queries pluginPath = os.path.dirname(__file__) @@ -115,7 +108,7 @@ def initAlgorithm(self, config=None): ) ) - # Output PostGIS layer + # Output PostGIS layer = summary table self.addOutput( QgsProcessingOutputVectorLayer( self.OUTPUT, @@ -140,46 +133,20 @@ def processAlgorithm(self, parameters, context, feedback): # Retrieve the output PostGIS layer name layer_name = self.parameterAsString(parameters, self.OUTPUT_NAME, context) - format_name = simplifyName(layer_name) + format_name = simplify_name(layer_name) feedback.pushInfo('Nom formaté : {}'.format(format_name)) # Retrieve the input vector layer = study area study_area = self.parameterAsVectorLayer(parameters, self.STUDY_AREA, context) # Check if the study area is a polygon layer - if QgsWkbTypes.displayString(study_area.wkbType()) not in ['Polygon', 'MultiPolygon']: - iface.messageBar().pushMessage("Erreur", "La zone d'étude fournie n'est pas valide ! Veuillez sélectionner une couche vecteur de type POLYGONE.", level=Qgis.Critical, duration=10) - raise QgsProcessingException(self.tr("La zone d'étude fournie n'est pas valide ! Veuillez sélectionner une couche vecteur de type POLYGONE.")) - # Retrieve the CRS - crs = study_area.dataProvider().crs().authid().split(':')[1] - # Retrieve the potential features selection - if len(study_area.selectedFeatures()) > 0: - selection = study_area.selectedFeatures() # Get only the selected features - else: - selection = study_area.getFeatures() # If there is no feature selected, get all of them + check_layer_geometry(study_area) # Define the name of the output PostGIS layer (summary table) which will be loaded in the QGis project layer_name = "Tableau synthèse {}".format(study_area.name()) - # Initialization of the "where" clause of the SQL query, aiming to retrieve the data for the summary table - where = "and (" - # Format the geometry of src_lpodatas.observations if different from the study area - if crs == '2154': - geom = "geom" - else: - geom = "st_transform(geom, {})".format(crs) - # For each entity in the study area... - for feature in selection: - # Retrieve the geometry - area = feature.geometry() # QgsGeometry object - # Retrieve the geometry type (single or multiple) - geomSingleType = QgsWkbTypes.isSingleType(area.wkbType()) - # Increment the "where" clause - if geomSingleType: - where = where + "st_within({}, ST_PolygonFromText('{}', {})) or ".format(geom, area.asWkt(), crs) - else: - where = where + "st_within({}, ST_MPolyFromText('{}', {})) or ".format(geom, area.asWkt(), crs) - # Remove the last "or" in the "where" clause which is useless - where = where[:len(where)-4] + ")" - #feedback.pushInfo('Clause where : {}'.format(where)) + # Construct the sql array containing the study area's features geometry + array_polygons = construct_sql_array_polygons(study_area) + # Define the "where" clause of the SQL query, aiming to retrieve the output PostGIS layer = summary table + where = "is_valid and st_within(geom, st_union({}))".format(array_polygons) # Retrieve the data base connection name connection = self.parameterAsString(parameters, self.DATABASE, context) @@ -187,6 +154,7 @@ def processAlgorithm(self, parameters, context, feedback): uri = postgis.uri_from_name(connection) # Retrieve the boolean add_table = self.parameterAsBool(parameters, self.ADD_TABLE, context) + if add_table: # Define the name of the PostGIS summary table which will be created in the DB table_name = "summary_table_{}".format(study_area.name()) @@ -197,64 +165,37 @@ def processAlgorithm(self, parameters, context, feedback): select row_number() OVER () AS id, source_id_sp, nom_sci, nom_vern, count(*) as nb_observations, count(distinct(observateur)) as nb_observateurs, max(date_an) as derniere_observation from src_lpodatas.observations - where is_valid {} + where {} group by source_id_sp, nom_sci, nom_vern order by source_id_sp)""".format(table_name, where), "alter table {} add primary key (id)".format(table_name) ] # Execute the SQL queries - for query in queries: - processing.run( - 'qgis:postgisexecutesql', - { - 'DATABASE': connection, - 'SQL': query - }, - is_child_algorithm=True, - context=context, - feedback=feedback - ) - feedback.pushInfo('Requête SQL exécutée avec succès !') + execute_sql_queries(context, feedback, connection, queries) # Format the URI uri.setDataSource(None, table_name, None, "", "id") + else: # Define the SQL queries query = """(select row_number() OVER () AS id, source_id_sp, nom_sci, nom_vern, count(*) as nb_observations, count(distinct(observateur)) as nb_observateurs, max(date_an) as derniere_observation from src_lpodatas.observations - where is_valid {} + where {} group by source_id_sp, nom_sci, nom_vern order by source_id_sp)""".format(where) # Format the URI uri.setDataSource("", query, None, "", "id") - # Retrieve the output PostGIS layer (summary table) which has just been created - layer_summary = QgsVectorLayer(uri.uri(), layer_name, "postgres") + # Retrieve the output PostGIS layer = summary table + layer_summary = QgsVectorLayer(uri.uri(), layer_name, "postgres") # Check if the PostGIS layer is valid - if not layer_summary.isValid(): - raise QgsProcessingException(self.tr("""Cette couche n'est pas valide ! - Checker les logs de PostGIS pour visualiser les messages d'erreur.""")) - else: - feedback.pushInfo('La couche PostGIS demandée est valide, la requête SQL a été exécutée avec succès !') - + check_layer_is_valid(feedback, layer_summary) # Load the PostGIS layer - root = context.project().layerTreeRoot() - plugin_lpo_group = root.findGroup('Résultats plugin LPO') - if not plugin_lpo_group: - plugin_lpo_group = root.insertGroup(0, 'Résultats plugin LPO') - context.project().addMapLayers([layer_summary], False) - plugin_lpo_group.addLayer(layer_summary) - # Variant - # context.temporaryLayerStore().addMapLayer(layer_summary) - # context.addLayerToLoadOnCompletion( - # layer_summary.id(), - # QgsProcessingContext.LayerDetails(layer_name, context.project(), self.OUTPUT) - # ) - + load_layer(context, layer_summary) # Open the attribute table of the PostGIS layer iface.setActiveLayer(layer_summary) iface.showAttributeTable(layer_summary) - + return {self.OUTPUT: layer_summary.id()} def tr(self, string): From 957ae22f2cbbc844f77a207390c4a5ddf742a372 Mon Sep 17 00:00:00 2001 From: lpofredc Date: Mon, 4 May 2020 22:30:57 +0200 Subject: [PATCH 05/19] apply gitignore --- .gitignore | 118 ++++++++++++++++++ __pycache__/__init__.cpython-36.pyc | Bin 1676 -> 0 bytes __pycache__/common_functions.cpython-36.pyc | Bin 4339 -> 0 bytes __pycache__/extract_data.cpython-36.pyc | Bin 4811 -> 0 bytes __pycache__/histogram.cpython-36.pyc | Bin 6053 -> 0 bytes __pycache__/scripts_lpo.cpython-36.pyc | Bin 2285 -> 0 bytes .../scripts_lpo_algorithm.cpython-36.pyc | Bin 5302 -> 0 bytes .../scripts_lpo_provider.cpython-36.pyc | Bin 3527 -> 0 bytes __pycache__/summary_table.cpython-36.pyc | Bin 6328 -> 0 bytes 9 files changed, 118 insertions(+) create mode 100644 .gitignore delete mode 100644 __pycache__/__init__.cpython-36.pyc delete mode 100644 __pycache__/common_functions.cpython-36.pyc delete mode 100644 __pycache__/extract_data.cpython-36.pyc delete mode 100644 __pycache__/histogram.cpython-36.pyc delete mode 100644 __pycache__/scripts_lpo.cpython-36.pyc delete mode 100644 __pycache__/scripts_lpo_algorithm.cpython-36.pyc delete mode 100644 __pycache__/scripts_lpo_provider.cpython-36.pyc delete mode 100644 __pycache__/summary_table.cpython-36.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..265b122 --- /dev/null +++ b/.gitignore @@ -0,0 +1,118 @@ + +# Created by https://www.gitignore.io/api/code,python +# Edit at https://www.gitignore.io/?templates=code,python + +### Code ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# End of https://www.gitignore.io/api/code,python + diff --git a/__pycache__/__init__.cpython-36.pyc b/__pycache__/__init__.cpython-36.pyc deleted file mode 100644 index 7250c3f5ed0d4b150073f9c390293535198ab449..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1676 zcmb_d%}yIJ5cY-y=oX}2s-CKj#3hMHmO|A-s45VJ2nkVW+Di_s%-Wk+IO~<|E!tdL z9-|M?SIU)BU!kXtH$OyFMX1U|o*jF}pJ&FN$9;O5GA$>MpS|tH|=8(ye%eEwh#Hq`S;kJEZo6zAO{s2ld5~4-@J{TBO#ToPC5A zgrP`;4Z~)d&kP!W(@gh)T0RAueOs`-wY}BYdeeCQc4m2?(p-yP-_FcXXhHR$R#?Mj z4u{fEXlFu7o{jI|GoOHw`X7o7C^egFS^ zUdKRj^XI-WkZRS_bO0#EnsYEJw%1hiUC31i5lujICXChMGQ$fK7HGl(rD33$h;x7m z7%H-aaSgW5!E!w?pyFcEKK%l1o^VYioMo3%L~tS^o)``k&vz@P&zZZ%DVW1Ja^7?% z-oTMUbZUi4c7Y4jYxv5w!8vSC51OEE0($3Y;#rki7mEg!y8Sf_AP0%BZwApU%pYbG zu!oHvt94+Dqre|ur76l0Tp0=~#gLmK|EbJ+BH4riy%I^!-QZQCt`o2d&fCYGraye~ zkMbG%chxxDctpMZ@~}14ywS6}&Tv+o9yx_0_jTg{hgn8u$v1*+${XICD9YgeMu;Re z?rEw4#6sfx9QT~iRV;RzqQYA#EuL^pBi@4jA}mY;-mqIi$yl01yg4?ud5UXe+w-0| zp4t`Knx|c##TY7u$xouOeUjf7L0=7czUWOC@k=EZ9zf<>cHn0pl^v zqz%&C_Ei#$FTn(J?gw)nbF@~(c|;?tbpFaE$GJh4wad|IZDq6)hD=3aIP$}gW{9a# zUd2Ux$SAFcVKi-%IDI;IP7zRnQ-m_d@E?YZR^MSIXOh2vjx*yUE0@Y1{z_}*KXx0G AJ^%m! diff --git a/__pycache__/common_functions.cpython-36.pyc b/__pycache__/common_functions.cpython-36.pyc deleted file mode 100644 index f39ba4a8703ef400c438159e59b9de37e21b767e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4339 zcmb_f-EZ606(=c5qGj1}9Oq+Ok=-mGj>_SipQz#fKeKGL<^4E3 zy!V{*`<v;5J`Q943PztA?zbd;9y9HV1&9M5rjgihdjgdU}n zcuvq`bPCU-^f*0%=One(waQ87-#JYyRWH4|t3>+Oy34(0Bv#h$l6m5WjYb&QwK#Ai zFAT(Nvo#c)dG;Ev-F6~2P&jq2xl?Y;mu?9Sz0MnI{()DoUDc zwaB6`9I_mHzRz0ZqTJZ6`2MZRP{m@y@%+QD%lHvo&5Kug+MPs-;(K z{7X;o|NkCe!a?!!# zfL23J8X@&+EkgRrun-57aT0Aa5;5KoB&?~y^6HPsG7A`Ye6kjA`JPKwJeLK65eMIw zGh&-j>BfOFsbfj*X1%`~vJ^tQBLnO@VII~wdC0iHn9L16)Yp2MV0ZHB@9d%z$+K`0 zHs$*j9H2#fMBh-wtP+25(^vLlp_g5i%3j@7EwxYn4*;}F|HV`L>`Ne~J$h~_JmyX`6i}!c8Hd;+4(i0f0@z7;LctL$}-(||asd=yNiB(sZ zFXNJQXyRVLEnY(tX*)UvA<|qg*VQ}Pj?vS0avi;=gSO1}H3iyruhH~nd^vDc3$6;B z1|yHS7ey?Pp&~;h7L3m-@%TIxC%;L4oBTdGNIpwGPyUenG5I3-Q}XBJugTw%zyI?u z$v=`$e>OwXDaUabF3$_YIF4H_*S78WEZ(2HFn#gEk2~E5m7jj}lj_ay&b)o;@t5Zw z|L*+bPtVT&;LP>;w{BF-GzToCBMoPlMcfHQn)ky;3^QsZ!>VGW1&NoSo*o@cBJS+S z+49+e#>eofuWHrpu)(UVo^e)n)(>6BuZnFzuX=C!-F4Mmtpc7k&u5}aS5r z+hI^``f=S0L{(%|Y=gk6&tc+pO!N(DO9M(Pi4q#kG<5^Ng29hqG<249S^>9m)*I+Z zXJszx>D1aS^0_FtW5Qv1id+^Ic8W5aePZfbWKpxH|6HdY^D0=!Aw)F3vuY_JU9IK6}Xg)S}22^8M9{!6&f9kIMBCp2H&@164M0j`hs{#YY)f zxCB93Y5*!uoxS7mbhH_Z?K|0+=jE!kfZ+BJ1YDv;qE6XEg-h0qOR*_z*4?$$nq6lI zqKLPyU`1GHG;>lvrCU0eKy&3+T4T5<<6GP0<+jCezC+l!OWk=NGmtsoK+NXY?V z`^kYDqlW~AuplCK-Q+Vt&a8q0i=KE-MwRkXYDqV~8`MGsh*e{ZZaMBQ{}wj!Lce*! zR-5=f1p9cjfJQ4<(mBE05RYH;+y$&e8a$=>eioRSKe?m>oH^&SAe|VzzSMs^9j&uyFaQ|jsnd~6 z3ix>10A;}RKc)TB_=ZLcCn2v) zSZx0|g8*vUh{tY}*>KM%4(Cn_F&;#Y7l7u~!m~Dso`Xm_(!HO=H}!aGUy+al$yIbID#qu99w-RI0dY2_3{WxJo%&I^B?r z)rHH&1&neiBlMnuaVBMU^g3MKGjt6Bke99nqnSWNzMa{AC3Gl|uK1I9f%2UjgFOPI z3?A+PA$>ACj*s6@4g~U#6QNL58JFO{g@VP>YNe1C`){OUO5X-b*85=?r6V;jpvydr zo9QUBG~-b=C_9wiahfZ#q7kWuk?NwVD9Uhx`!h$FTxyxfJ}Gm%U6;z9hE&PNVX{=f zuvt+?qnTyBq?>wKpVaw0#;&xeyd4BxqbgaCkHT{?wVOi)-gIJHGYl4tfQdu zNnY3X3KAx;dR|$w0819z(+U`i`wT3Je+=Lk)b@hF>truxARN(;^Ame01?QVc19-Ab z9#WDlZ{OOu_0FyJ#WV-%-2llb~eh=o9$k4|7Xn8-^^U zWIRhSt{ZcX}Q@r z@_dmM4)T%d5JLO^Zk(6JP7RW)I~g}-sJ&u+-G csOsDVF7eBmM%kFkSvhOe8nMb~k6TCo1FSNH&j0`b diff --git a/__pycache__/extract_data.cpython-36.pyc b/__pycache__/extract_data.cpython-36.pyc deleted file mode 100644 index d08993105276ca4f03121981f67558bbc107f1a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4811 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 CKO6D@ diff --git a/__pycache__/scripts_lpo_algorithm.cpython-36.pyc b/__pycache__/scripts_lpo_algorithm.cpython-36.pyc deleted file mode 100644 index c344da5a2a31f54e5e69ac6c640f4b62a6692521..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5302 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 f)*k2Nl@$dTfr(zxCKWFQ4X`*VxRSSGz?2 z&Us#?v2t-wg^ZrPmKJ?$wsxMAbz;(wQ1(^W*Xp%kT-W=9OnL3YCmVZ2Egu`r`gL;q z*6mwsw|-dr{>M!7q3RE`=yYwS2Esa7epqwbWPd;&OGC+KETrTEvW(7c#4k2#nTf|e zD&*vJl-CLPy+&hA?}Jcp>k7Fzz*ZWS3*La0|N8&GzgHkonEbLU41sOyXxbwnwl(L( zsJ1<#n%^Y@6_XZ?h~`WftHpi{8zwBF5o;(-dWwnmfRK?f7>Fa-eqy_vSgw19sJ8pq z+(9Y z&9#AVa(lec$m%Kq@9-IUwoEO?qKWF`{2Bx>AQG}KGf{WypKeCRKCSe*qiF2no~_qKRxNIfh&mv- zl7H`m9*oXDKDL^+ER;5d&+1;>>rp*;N%tj(@A6Z~BbRM0G(3I+5bnck?m@#n;7bqq z5?J%C2b>A43BBV@=z%p^3Hll2N$87gmQ|rIu{q$q`Q*wOJ&m1fp>z0Pk83@I*UUp> zy#xQmJN1EGeV82#zsvaI5pUC2S^`l+>c=gsbUho~t_2$S)k{iYC?a9Q5Q57aF5B-s zJqrW>95lMB#~ZwpGE$>1Rg21o=|W*Ob~m5xx#ms-imxpsHw~7q*XR#yS4EA!j5{JS zjR}68lqb1Xmtnybcn!wp<%c&ugrGk5^Bfk4hr98Zr5|njjZK=bkTuPT1X^u@t%A$FPxBw^5_ARN5@PzZh?NiOy z6^Px*X#?lEmjr^{f<6UwX1Fi~P{GbYnWYt)JMj(x&eJrd{>WUO1ayoMiD(Z1IO>X4 z7a7=rY87D|J%uQc2*=8qr6b@!5vAPv>RG*#4LCyo0B8p zhSG3xCoAyr0Z#xi$nn7Hk3g2kKz_fi*v@O4U&z zgkA4PomxTT4kv*!Nxn~QH_0C;5hazhPIus+kKuyWHZoiaQ*l9^bGS^a%;PMCSCLed zrIk4jvrabPP_vxsHKvXtC<)#-aB?5U0*-GyTBl~xSv87YU-BMQGHAo$%Fhtl{#2>V z#Lk(g6A^wjW#?0^EXEwDSxL59VaLxM_4sO%^Wg#q`DeJ)i5{0V%dAnx>%lip3UiQBcj#`M!O*c72js+6&f zv6WiRQA1KWvzz3dj08VH7W23n*);PUMokGCFMrvu`17UV*HiE~KWDpE#-jwwBMg&D z7{b*mmMB-l@Gzz_9dS&r@!-;zv023CIyN`3IrGa&$YdHFS%%l(S1BmXluD&?saPty zk=jC13`3?`>8dn_vqysrzQ9!r*Pjc+*7)+Bx}*7Pfw!(O)K|b}atW!qzBAah^<4`W z@U&3M?%$TuJUu9@JiQp8UFvX8=1h7y2rb?Y+%_a-*sJt{;C4h`#TAN7XaoS=!ow0) nUV#5y1k-{}O7~LyenwU?UqX`)@;>}>`Jm)igR1X-K{fX;ofd7p diff --git a/__pycache__/summary_table.cpython-36.pyc b/__pycache__/summary_table.cpython-36.pyc deleted file mode 100644 index 0648d16264ae63a181e8afe287d10c8c0b4ae3af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6328 zcmeHLOLH5?5ymc_Ts#O;6!o^(uQH*MNX2$JRdV7OmMB?OGDVq`o$Mi7%bfwZ)IOlu zSy6z%OWFJrCp8mRf zKCIV`zdrcWumApLF89ye%u_)9GyLN}qhL8J$23c0x}~$cm1hO3z>2Ko7p)@RWxo`Z zt+JLwz2aAb1#3ao7kneATGgOt)q=WJ4;oe@ShN;{rqv9VtfgStT2^$1e=1n9R#d&} zpAJ^7Rn%*&?w<+HT4#fE*16!kbw0RYT?j5(7lTXIB`wE`2X7p_$<2{wy>&as8f@{~ z99!hIQQmr+m3DHirt`0SE@y09etnN&j-MS@cs&{4-M(jDH{&D-9MQL>v*+{mUVln; z_2e_{yXiBo+F zJMny<_sun0RXh2r-I}u43>?p&9ZvP@rr(RMI*D*TLuI`q4D_>^|I-w~z^I|Eyy#xyLr0IlgBNAp1 zF|X4%%`r1{Bq8I%l>6M2Tm-Qhb=1f0tq1099&+LM=615@d#-ucb9oqZ)4}}I67O?H zYMd}59=B*UJLA=uw<2(Nq!)#snA}6ZFduUfecGpN3MD-wYtufO-j zyWau%a$7AeE#AN5V&}B9dH+Tvc)Qnw7L_(ym9&1p8*huq<#FtV-E?*Cv+Z{y0euDO z#kuxv2ZxSB2Rk@f81>f6d%xzcjKp21&qeyi9Lc>z_7XYUe`&6NI>F~rGM{4SyV z;I|4}6@)kw9h=g5$YawnA#w=Kk#))l->_WA+;hm{Lx%6iO0DK(7hsLpNa|Be1(%5k ziO<-vm?0RAw;AZM4c*+nvy+WlS3}au&Gv5l^Y+eWYV7PjxcMu){nchWU1;CDX`}t_ zW?H)UV0Zh$Zd%RW_E!7LO_#Jw554Js1OJ#DKhq$xz9mkh{w#+BQHZ(U5f|{OihsKs z`_TFZ?`A${W8IHj$KQze0i7H7u{u?JxB+Jj>itm>2CFv&Cs+GoH>qBzoI`JmF(NP!uhTtJVZdP^gXm8U&b zSwm}G^;EKP)4qaQMML#1jIz}jSdHS!syG_80iv++^92{&Jme-!#|sfuASm!cfIg4` zC=w|LF!B9U47y@EeiU}6Ob+3E5YzS{%q2+vSKAex@u?e}=*VcTK<_SjE~1tK_01Pd6QIdV~<#&cAh zN72eFU`k6c+K$&vD{Rm1QOJ;?l)_;JRdp})qASVr5O#wsPX|rupfbu~Q~_3n)}u1ZqqU4$Wy;@B;}&G~pf-f<9FgtS z6~~6^sSc~+AFMFLt;UM@s=+!-^d445IxA-_-TYBqE)Ml!ez-8Kj|#A9w8JFW z<6^C4-ursz$LzRw1?C;tvFl|OpbY~8=gArjoZAj1OaLED>On2Dd{3^SBcOy> zxD?J1JjCmhnXsSnN%~CF*rpTGC~rq8;g4*@$&rzuEcDpO*5Dn-r;P7~Vi-g2>(RZ1 z`H(IRJl5Jufp8uc4n?m0UrE_v9h>9-LP=>wc}l_K*7eNsJjtKPw9?_6?K$p4B#A1~RH%`q}hJ@0S8I`h1xCOmC3-_$Gcy%pU+ z&Cr|56IaTHR?9c^l?6jzR_%svXofzxH0ODb^PUE^6x>o^tAbkEZ3(x_v?0P-m9C}6 z_A~-(>4Hep1t}0D(RB}6DA`F%2&r)8a7-j|6@7off0FDh&aFv`mq+px3&`9khxs+! zl&FO9d-9V;<{R$dHoys8{+qim5pf@|M+4iHv@#$dT zt8CjWa&0?1-zp5FDC zF;xl$gjVC9Udc~>#>bT}D*0?H-Vwh9B`w-XkoXEL6QU^3VyAW6b|-hcnKXhw_UKX% zLcB%e79Sx3Uf=HDm+SW>?srvgXfKxjmYFA% y%|c?j1b|TQQYT$1yli*V%Fi;*eo8!Iih~Qf+TMIYNA#-Kw6#i2t7)cQ%l`)^ESwks From 9cf693667511dfa4412b54b0ce66e5f03fa48363 Mon Sep 17 00:00:00 2001 From: lpofredc Date: Mon, 4 May 2020 22:51:42 +0200 Subject: [PATCH 06/19] WIP script on selected area --- extract_data.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extract_data.py b/extract_data.py index c8e915a..41296e6 100644 --- a/extract_data.py +++ b/extract_data.py @@ -35,6 +35,7 @@ QgsProcessingParameterString, QgsProcessingParameterVectorLayer, QgsProcessingOutputVectorLayer, + QgsProcessingParameterFeatureSource, QgsDataSourceUri, QgsVectorLayer) from processing.tools import postgis @@ -89,7 +90,7 @@ def initAlgorithm(self, config=None): # Input vector layer = study area self.addParameter( - QgsProcessingParameterVectorLayer( + QgsProcessingParameterFeatureSource( self.STUDY_AREA, self.tr("Zone d'étude"), [QgsProcessing.TypeVectorAnyGeometry] From 8ddffb087c28203ffb7475b993c6648c82cb80a1 Mon Sep 17 00:00:00 2001 From: eguilley Date: Tue, 5 May 2020 10:36:01 +0200 Subject: [PATCH 07/19] Utilisation de QgsProcessingParameterFeatureSource --- common_functions.py | 2 +- extract_data.py | 9 +++++---- summary_table.py | 29 +++++++++++++++-------------- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/common_functions.py b/common_functions.py index b13158c..a924a13 100644 --- a/common_functions.py +++ b/common_functions.py @@ -85,7 +85,7 @@ def construct_sql_array_polygons(layer): # Initialization of the sql array containing the study area's features geometry array_polygons = "array[" # For each entity in the study area... - for feature in set_features(layer): + for feature in layer.getFeatures(): # Retrieve the geometry area = feature.geometry() # QgsGeometry object # Retrieve the geometry type (single or multiple) diff --git a/extract_data.py b/extract_data.py index 41296e6..410f704 100644 --- a/extract_data.py +++ b/extract_data.py @@ -33,9 +33,8 @@ from qgis.core import (QgsProcessing, QgsProcessingAlgorithm, QgsProcessingParameterString, - QgsProcessingParameterVectorLayer, - QgsProcessingOutputVectorLayer, QgsProcessingParameterFeatureSource, + QgsProcessingOutputVectorLayer, QgsDataSourceUri, QgsVectorLayer) from processing.tools import postgis @@ -91,6 +90,7 @@ def initAlgorithm(self, config=None): # Input vector layer = study area self.addParameter( QgsProcessingParameterFeatureSource( + #QgsProcessingParameterVectorLayer( self.STUDY_AREA, self.tr("Zone d'étude"), [QgsProcessing.TypeVectorAnyGeometry] @@ -112,7 +112,8 @@ def processAlgorithm(self, parameters, context, feedback): """ # Retrieve the input vector layer = study area - study_area = self.parameterAsVectorLayer(parameters, self.STUDY_AREA, context) + #study_area = self.parameterAsVectorLayer(parameters, self.STUDY_AREA, context) + study_area = self.parameterAsSource(parameters, self.STUDY_AREA, context) # Check if the study area is a polygon layer check_layer_geometry(study_area) @@ -128,7 +129,7 @@ def processAlgorithm(self, parameters, context, feedback): uri.setDataSource("src_lpodatas", "observations", "geom", where) # Retrieve the output PostGIS layer = biodiversity data - layer_obs = QgsVectorLayer(uri.uri(), "Données d'observations {}".format(study_area.name()), "postgres") + layer_obs = QgsVectorLayer(uri.uri(), "Données d'observations {}".format(study_area.sourceName()), "postgres") # Check if the PostGIS layer is valid check_layer_is_valid(feedback, layer_obs) # Load the PostGIS layer diff --git a/summary_table.py b/summary_table.py index 8420332..63d35c9 100644 --- a/summary_table.py +++ b/summary_table.py @@ -28,12 +28,13 @@ import os from qgis.PyQt.QtGui import QIcon +from qgis.utils import iface from qgis.PyQt.QtCore import QCoreApplication from qgis.core import (QgsProcessing, QgsProcessingAlgorithm, QgsProcessingParameterString, - QgsProcessingParameterVectorLayer, + QgsProcessingParameterFeatureSource, QgsProcessingOutputVectorLayer, QgsProcessingParameterBoolean, QgsDataSourceUri, @@ -92,22 +93,13 @@ def initAlgorithm(self, config=None): # Input vector layer = study area self.addParameter( - QgsProcessingParameterVectorLayer( + QgsProcessingParameterFeatureSource( self.STUDY_AREA, self.tr("Zone d'étude"), [QgsProcessing.TypeVectorAnyGeometry] ) ) - # Boolean : True = add the summary table in the DB ; False = don't - self.addParameter( - QgsProcessingParameterBoolean( - self.ADD_TABLE, - self.tr("Enregistrer les données en sortie dans une nouvelle table PostgreSQL"), - False - ) - ) - # Output PostGIS layer = summary table self.addOutput( QgsProcessingOutputVectorLayer( @@ -126,6 +118,15 @@ def initAlgorithm(self, config=None): ) ) + # Boolean : True = add the summary table in the DB ; False = don't + self.addParameter( + QgsProcessingParameterBoolean( + self.ADD_TABLE, + self.tr("Enregistrer les données en sortie dans une nouvelle table PostgreSQL"), + False + ) + ) + def processAlgorithm(self, parameters, context, feedback): """ Here is where the processing itself takes place. @@ -137,11 +138,11 @@ def processAlgorithm(self, parameters, context, feedback): feedback.pushInfo('Nom formaté : {}'.format(format_name)) # Retrieve the input vector layer = study area - study_area = self.parameterAsVectorLayer(parameters, self.STUDY_AREA, context) + study_area = self.parameterAsSource(parameters, self.STUDY_AREA, context) # Check if the study area is a polygon layer check_layer_geometry(study_area) # Define the name of the output PostGIS layer (summary table) which will be loaded in the QGis project - layer_name = "Tableau synthèse {}".format(study_area.name()) + layer_name = "Tableau synthèse {}".format(study_area.sourceName()) # Construct the sql array containing the study area's features geometry array_polygons = construct_sql_array_polygons(study_area) @@ -157,7 +158,7 @@ def processAlgorithm(self, parameters, context, feedback): if add_table: # Define the name of the PostGIS summary table which will be created in the DB - table_name = "summary_table_{}".format(study_area.name()) + table_name = "summary_table_{}".format(study_area.sourceName()) # Define the SQL queries queries = [ "drop table if exists {}".format(table_name), From 266d8a3f6ae2a098ba3754872e1127aa6412e950 Mon Sep 17 00:00:00 2001 From: eguilley Date: Tue, 5 May 2020 14:44:40 +0200 Subject: [PATCH 08/19] Standardisation des noms de couches en sortie --- common_functions.py | 13 +----- extract_data.py | 21 ++++++++-- histogram.py | 96 +++++++++++++++++++++++++-------------------- summary_table.py | 20 +++++----- 4 files changed, 80 insertions(+), 70 deletions(-) diff --git a/common_functions.py b/common_functions.py index a924a13..1b61ec3 100644 --- a/common_functions.py +++ b/common_functions.py @@ -67,17 +67,6 @@ def check_layer_is_valid(feedback, layer): feedback.pushInfo("La couche PostGIS demandée est valide, la requête SQL a été exécutée avec succès !") return None -def set_features(layer): - """ - Retrieve the selected features of the input vector layer. - """ - if len(layer.selectedFeatures()) > 0: - selection = layer.selectedFeatures() - else: - # If there is no feature selected, get all of them - selection = layer.getFeatures() - return selection - def construct_sql_array_polygons(layer): """ Construct the sql array containing the input vector layer's features geometry. @@ -131,5 +120,5 @@ def execute_sql_queries(context, feedback, connection, queries): context=context, feedback=feedback ) - feedback.pushInfo('Requête SQL exécutée avec succès !') + feedback.pushInfo('Requête SQL exécutée avec succès !') return None \ No newline at end of file diff --git a/extract_data.py b/extract_data.py index 410f704..c9db7b0 100644 --- a/extract_data.py +++ b/extract_data.py @@ -27,6 +27,7 @@ __revision__ = '$Format:%H$' import os +from datetime import datetime from qgis.PyQt.QtGui import QIcon from qgis.PyQt.QtCore import QCoreApplication @@ -53,6 +54,7 @@ class ExtractData(QgsProcessingAlgorithm): DATABASE = 'DATABASE' STUDY_AREA = 'STUDY_AREA' OUTPUT = 'OUTPUT' + OUTPUT_NAME = 'OUTPUT_NAME' def name(self): return 'ExtractData' @@ -90,7 +92,6 @@ def initAlgorithm(self, config=None): # Input vector layer = study area self.addParameter( QgsProcessingParameterFeatureSource( - #QgsProcessingParameterVectorLayer( self.STUDY_AREA, self.tr("Zone d'étude"), [QgsProcessing.TypeVectorAnyGeometry] @@ -106,19 +107,31 @@ def initAlgorithm(self, config=None): ) ) + # Output PostGIS layer name + self.addParameter( + QgsProcessingParameterString( + self.OUTPUT_NAME, + self.tr("Nom de la couche en sortie"), + self.tr("Données d'observation") + ) + ) + def processAlgorithm(self, parameters, context, feedback): """ Here is where the processing itself takes place. """ # Retrieve the input vector layer = study area - #study_area = self.parameterAsVectorLayer(parameters, self.STUDY_AREA, context) study_area = self.parameterAsSource(parameters, self.STUDY_AREA, context) # Check if the study area is a polygon layer check_layer_geometry(study_area) + # Retrieve the output PostGIS layer name and format it + layer_name = self.parameterAsString(parameters, self.OUTPUT_NAME, context) + ts = datetime.now() + format_name = layer_name + " " + str(ts.strftime('%s')) # Construct the sql array containing the study area's features geometry - array_polygons = construct_sql_array_polygons(study_area) + array_polygons = construct_sql_array_polygons(study_area) # Define the "where" clause of the SQL query, aiming to retrieve the output PostGIS layer = biodiversity data where = "is_valid and st_within(geom, st_union({}))".format(array_polygons) @@ -129,7 +142,7 @@ def processAlgorithm(self, parameters, context, feedback): uri.setDataSource("src_lpodatas", "observations", "geom", where) # Retrieve the output PostGIS layer = biodiversity data - layer_obs = QgsVectorLayer(uri.uri(), "Données d'observations {}".format(study_area.sourceName()), "postgres") + layer_obs = QgsVectorLayer(uri.uri(), format_name, "postgres") # Check if the PostGIS layer is valid check_layer_is_valid(feedback, layer_obs) # Load the PostGIS layer diff --git a/histogram.py b/histogram.py index 65c46d9..4f524bc 100644 --- a/histogram.py +++ b/histogram.py @@ -27,26 +27,23 @@ __revision__ = '$Format:%H$' import os +from datetime import datetime from qgis.PyQt.QtGui import QIcon +from qgis.utils import iface + +import matplotlib.pyplot as plt +import numpy as np from qgis.PyQt.QtCore import QCoreApplication from qgis.core import (QgsProcessing, QgsProcessingAlgorithm, QgsProcessingParameterString, - QgsProcessingParameterVectorLayer, + QgsProcessingParameterFeatureSource, QgsProcessingOutputVectorLayer, QgsDataSourceUri, - QgsVectorLayer, - QgsWkbTypes, - QgsProcessingContext, - QgsProcessingException) -from qgis.utils import iface + QgsVectorLayer) from processing.tools import postgis -from .common_functions import check_layer_geometry, set_features - -import processing -import matplotlib.pyplot as plt -import numpy as np +from .common_functions import check_layer_geometry, check_layer_is_valid, construct_sql_array_polygons, load_layer pluginPath = os.path.dirname(__file__) @@ -61,12 +58,13 @@ class Histogram(QgsProcessingAlgorithm): DATABASE = 'DATABASE' STUDY_AREA = 'STUDY_AREA' OUTPUT = 'OUTPUT' + OUTPUT_NAME = 'OUTPUT_NAME' def name(self): return 'Histogram' def displayName(self): - return 'Create an histogram' + return 'NE PAS TESTER - Create an histogram' def icon(self): return QIcon(os.path.join(pluginPath, 'icons', 'histogram.png')) @@ -97,59 +95,71 @@ def initAlgorithm(self, config=None): # Input vector layer = study area self.addParameter( - QgsProcessingParameterVectorLayer( + QgsProcessingParameterFeatureSource( self.STUDY_AREA, self.tr("Zone d'étude"), [QgsProcessing.TypeVectorAnyGeometry] ) ) + # Output PostGIS layer = histogram data + self.addOutput( + QgsProcessingOutputVectorLayer( + self.OUTPUT, + self.tr('Couche en sortie'), + QgsProcessing.TypeVectorAnyGeometry + ) + ) + + # Output PostGIS layer name + self.addParameter( + QgsProcessingParameterString( + self.OUTPUT_NAME, + self.tr("Nom de la couche en sortie"), + self.tr("Données histogramme") + ) + ) + def processAlgorithm(self, parameters, context, feedback): """ Here is where the processing itself takes place. """ # Retrieve the input vector layer = study area - study_area = self.parameterAsVectorLayer(parameters, self.STUDY_AREA, context) + study_area = self.parameterAsSource(parameters, self.STUDY_AREA, context) # Check if the study area is a polygon layer check_layer_geometry(study_area) - # Initialization of the "where" clause of the SQL query, aiming to retrieve the data for the histogram - where = "and (" - # For each entity in the study area... - for feature in set_features(study_area): - # Retrieve the geometry - area = feature.geometry() # QgsGeometry object - # Retrieve the geometry type (single or multiple) - geomSingleType = QgsWkbTypes.isSingleType(area.wkbType()) - # Increment the "where" clause - if geomSingleType: - where = where + "st_within(geom, ST_PolygonFromText('{}', 2154)) or ".format(area.asWkt()) - else: - where = where + "st_within(geom, ST_MPolyFromText('{}', 2154)) or ".format(area.asWkt()) - # Remove the last "or" in the "where" clause which is useless - where = where[:len(where)-4] + ")" - #feedback.pushInfo('Clause where : {}'.format(where)) + # Retrieve the output PostGIS layer name and format it + layer_name = self.parameterAsString(parameters, self.OUTPUT_NAME, context) + ts = datetime.now() + format_name = layer_name + " " + str(ts.strftime('%s')) + + # Construct the sql array containing the study area's features geometry + array_polygons = construct_sql_array_polygons(study_area) + # Define the "where" clause of the SQL query, aiming to retrieve the output PostGIS layer = histogram data + where = "is_valid and st_within(geom, st_union({}))".format(array_polygons) + # Retrieve the data base connection name + connection = self.parameterAsString(parameters, self.DATABASE, context) + # Define the SQL query query = """(select groupe_taxo, count(*) as nb_observations from src_lpodatas.observations - where is_valid {} + where {} group by groupe_taxo order by count(*) desc)""".format(where) - - # Retrieve the data base connection name - connection = self.parameterAsString(parameters, self.DATABASE, context) - # Retrieve the data for the histogram through a layer - # URI --> Configures connection to database and the SQL query + # URI --> Configures connection to database and the SQL query uri = postgis.uri_from_name(connection) uri.setDataSource("", query, None, "", "groupe_taxo") - layer_histo = QgsVectorLayer(uri.uri(), "Histogram", "postgres") + # Retrieve the output PostGIS layer = histogram data + layer_histo = QgsVectorLayer(uri.uri(), format_name, "postgres") # Check if the PostGIS layer is valid - if not layer_histo.isValid(): - raise QgsProcessingException(self.tr("""Cette couche n'est pas valide ! - Checker les logs de PostGIS pour visualiser les messages d'erreur.""")) - else: - feedback.pushInfo('La couche PostGIS demandée est valide, la requête SQL a été exécutée avec succès !') + check_layer_is_valid(feedback, layer_histo) + # Load the PostGIS layer + load_layer(context, layer_histo) + # Open the attribute table of the PostGIS layer + iface.setActiveLayer(layer_histo) + iface.showAttributeTable(layer_histo) plt.rcdefaults() libel = [feature['groupe_taxo'] for feature in layer_histo.getFeatures()] @@ -168,7 +178,7 @@ def processAlgorithm(self, parameters, context, feedback): ax.set_title(u'Etat des connaissances par groupes d\'espèces') plt.show() - return {} + return {self.OUTPUT: layer_histo.id()} def tr(self, string): return QCoreApplication.translate('Processing', string) diff --git a/summary_table.py b/summary_table.py index 63d35c9..98cc987 100644 --- a/summary_table.py +++ b/summary_table.py @@ -27,6 +27,7 @@ __revision__ = '$Format:%H$' import os +from datetime import datetime from qgis.PyQt.QtGui import QIcon from qgis.utils import iface @@ -114,7 +115,7 @@ def initAlgorithm(self, config=None): QgsProcessingParameterString( self.OUTPUT_NAME, self.tr("Nom de la couche en sortie"), - self.tr("tableau_synthese") + self.tr("Tableau synthèse") ) ) @@ -132,17 +133,14 @@ def processAlgorithm(self, parameters, context, feedback): Here is where the processing itself takes place. """ - # Retrieve the output PostGIS layer name - layer_name = self.parameterAsString(parameters, self.OUTPUT_NAME, context) - format_name = simplify_name(layer_name) - feedback.pushInfo('Nom formaté : {}'.format(format_name)) - # Retrieve the input vector layer = study area study_area = self.parameterAsSource(parameters, self.STUDY_AREA, context) # Check if the study area is a polygon layer check_layer_geometry(study_area) - # Define the name of the output PostGIS layer (summary table) which will be loaded in the QGis project - layer_name = "Tableau synthèse {}".format(study_area.sourceName()) + # Retrieve the output PostGIS layer name and format it + layer_name = self.parameterAsString(parameters, self.OUTPUT_NAME, context) + ts = datetime.now() + format_name = layer_name + " " + str(ts.strftime('%s')) # Construct the sql array containing the study area's features geometry array_polygons = construct_sql_array_polygons(study_area) @@ -158,7 +156,7 @@ def processAlgorithm(self, parameters, context, feedback): if add_table: # Define the name of the PostGIS summary table which will be created in the DB - table_name = "summary_table_{}".format(study_area.sourceName()) + table_name = simplify_name(format_name) # Define the SQL queries queries = [ "drop table if exists {}".format(table_name), @@ -177,7 +175,7 @@ def processAlgorithm(self, parameters, context, feedback): uri.setDataSource(None, table_name, None, "", "id") else: - # Define the SQL queries + # Define the SQL query query = """(select row_number() OVER () AS id, source_id_sp, nom_sci, nom_vern, count(*) as nb_observations, count(distinct(observateur)) as nb_observateurs, max(date_an) as derniere_observation from src_lpodatas.observations @@ -188,7 +186,7 @@ def processAlgorithm(self, parameters, context, feedback): uri.setDataSource("", query, None, "", "id") # Retrieve the output PostGIS layer = summary table - layer_summary = QgsVectorLayer(uri.uri(), layer_name, "postgres") + layer_summary = QgsVectorLayer(uri.uri(), format_name, "postgres") # Check if the PostGIS layer is valid check_layer_is_valid(feedback, layer_summary) # Load the PostGIS layer From 13600002c4676137938646c1d17817499da49ff5 Mon Sep 17 00:00:00 2001 From: eguilley Date: Tue, 5 May 2020 17:43:12 +0200 Subject: [PATCH 09/19] Ajout de colonnes pour Summary Table --- extract_data.py | 2 +- histogram.py | 12 ++++----- summary_table.py | 63 +++++++++++++++++++++++++++++++++++------------- 3 files changed, 53 insertions(+), 24 deletions(-) diff --git a/extract_data.py b/extract_data.py index c9db7b0..26b358c 100644 --- a/extract_data.py +++ b/extract_data.py @@ -133,7 +133,7 @@ def processAlgorithm(self, parameters, context, feedback): # Construct the sql array containing the study area's features geometry array_polygons = construct_sql_array_polygons(study_area) # Define the "where" clause of the SQL query, aiming to retrieve the output PostGIS layer = biodiversity data - where = "is_valid and st_within(geom, st_union({}))".format(array_polygons) + where = "is_valid and ST_within(geom, SY_union({}))".format(array_polygons) # Retrieve the data base connection name connection = self.parameterAsString(parameters, self.DATABASE, context) diff --git a/histogram.py b/histogram.py index 4f524bc..3a93ade 100644 --- a/histogram.py +++ b/histogram.py @@ -137,16 +137,16 @@ def processAlgorithm(self, parameters, context, feedback): # Construct the sql array containing the study area's features geometry array_polygons = construct_sql_array_polygons(study_area) # Define the "where" clause of the SQL query, aiming to retrieve the output PostGIS layer = histogram data - where = "is_valid and st_within(geom, st_union({}))".format(array_polygons) + where = "is_valid and ST_within(geom, ST_union({}))".format(array_polygons) # Retrieve the data base connection name connection = self.parameterAsString(parameters, self.DATABASE, context) # Define the SQL query - query = """(select groupe_taxo, count(*) as nb_observations - from src_lpodatas.observations - where {} - group by groupe_taxo - order by count(*) desc)""".format(where) + query = """(SELECT groupe_taxo, COUNT(*) AS nb_observations + FROM src_lpodatas.observations + WHERE {} + GROUP BY groupe_taxo + ORDER BY count(*) desc)""".format(where) # URI --> Configures connection to database and the SQL query uri = postgis.uri_from_name(connection) uri.setDataSource("", query, None, "", "groupe_taxo") diff --git a/summary_table.py b/summary_table.py index 98cc987..abe5be2 100644 --- a/summary_table.py +++ b/summary_table.py @@ -63,7 +63,7 @@ def name(self): return 'SummaryTable' def displayName(self): - return 'Create a summary table' + return 'Create a summary table per species' def icon(self): return QIcon(os.path.join(pluginPath, 'icons', 'summary_table.png')) @@ -145,7 +145,7 @@ def processAlgorithm(self, parameters, context, feedback): # Construct the sql array containing the study area's features geometry array_polygons = construct_sql_array_polygons(study_area) # Define the "where" clause of the SQL query, aiming to retrieve the output PostGIS layer = summary table - where = "is_valid and st_within(geom, st_union({}))".format(array_polygons) + where = "is_valid and ST_within(geom, ST_union({}))".format(array_polygons) # Retrieve the data base connection name connection = self.parameterAsString(parameters, self.DATABASE, context) @@ -159,15 +159,29 @@ def processAlgorithm(self, parameters, context, feedback): table_name = simplify_name(format_name) # Define the SQL queries queries = [ - "drop table if exists {}".format(table_name), - """create table {} as ( - select row_number() OVER () AS id, source_id_sp, nom_sci, nom_vern, - count(*) as nb_observations, count(distinct(observateur)) as nb_observateurs, max(date_an) as derniere_observation - from src_lpodatas.observations - where {} - group by source_id_sp, nom_sci, nom_vern - order by source_id_sp)""".format(table_name, where), - "alter table {} add primary key (id)".format(table_name) + "DROP TABLE if exists {}".format(table_name), + """CREATE TABLE {} AS (WITH data AS + (SELECT source_id_sp, nom_sci AS nom_scientifique, nom_vern AS nom_vernaculaire, groupe_taxo, + COUNT(*) AS nb_donnees, COUNT(distinct(observateur)) AS nb_observateurs, + COALESCE(SUM(CASE WHEN mortalite THEN 1 ELSE 0 END),0) AS nb_mortalite, + max(sn.code_nidif) AS max_atlas_code, max(nombre_total) AS nb_individus_max, + min (date_an) as premiere_observation, max(date_an) as derniere_observation, + string_agg(distinct source,', ') as sources + FROM src_lpodatas.observations obs + LEFT JOIN referentiel.statut_nidif sn ON obs.oiso_code_nidif = sn.code_repro + WHERE {} + GROUP BY source_id_sp, nom_sci, nom_vern, groupe_taxo), + synthese AS + (SELECT DISTINCT source_id_sp, nom_scientifique, nom_vernaculaire, groupe_taxo, + nb_donnees, nb_observateurs, nb_mortalite, + sn2.statut_nidif, nb_individus_max, + premiere_observation, derniere_observation, sources + FROM data d + LEFT JOIN referentiel.statut_nidif sn2 ON d.max_atlas_code = sn2.code_nidif + ORDER BY groupe_taxo, source_id_sp) + SELECT row_number() OVER () AS id, * + FROM synthese)""".format(table_name, where), + "ALTER TABLE {} add primary key (id)".format(table_name) ] # Execute the SQL queries execute_sql_queries(context, feedback, connection, queries) @@ -176,12 +190,27 @@ def processAlgorithm(self, parameters, context, feedback): else: # Define the SQL query - query = """(select row_number() OVER () AS id, source_id_sp, nom_sci, nom_vern, - count(*) as nb_observations, count(distinct(observateur)) as nb_observateurs, max(date_an) as derniere_observation - from src_lpodatas.observations - where {} - group by source_id_sp, nom_sci, nom_vern - order by source_id_sp)""".format(where) + query = """(WITH data AS + (SELECT source_id_sp, nom_sci AS nom_scientifique, nom_vern AS nom_vernaculaire, groupe_taxo, + COUNT(*) AS nb_donnees, COUNT(distinct(observateur)) AS nb_observateurs, + COALESCE(SUM(CASE WHEN mortalite THEN 1 ELSE 0 END),0) AS nb_mortalite, + max(sn.code_nidif) AS max_atlas_code, max(nombre_total) AS nb_individus_max, + min (date_an) as premiere_observation, max(date_an) as derniere_observation, + string_agg(distinct source,', ') as sources + FROM src_lpodatas.observations obs + LEFT JOIN referentiel.statut_nidif sn ON obs.oiso_code_nidif = sn.code_repro + WHERE {} + GROUP BY source_id_sp, nom_sci, nom_vern, groupe_taxo), + synthese AS + (SELECT DISTINCT source_id_sp, nom_scientifique, nom_vernaculaire, groupe_taxo, + nb_donnees, nb_observateurs, nb_mortalite, + sn2.statut_nidif, nb_individus_max, + premiere_observation, derniere_observation, sources + FROM data d + LEFT JOIN referentiel.statut_nidif sn2 ON d.max_atlas_code = sn2.code_nidif + ORDER BY groupe_taxo, source_id_sp) + SELECT row_number() OVER () AS id, * + FROM synthese)""".format(where) # Format the URI uri.setDataSource("", query, None, "", "id") From 2c3dec184aded0c5c304071439a47c92238206a8 Mon Sep 17 00:00:00 2001 From: eguilley Date: Wed, 6 May 2020 16:16:55 +0200 Subject: [PATCH 10/19] Optimisation des algos pour 1ers tests --- common_functions.py | 22 +++++----- extract_data.py | 22 +++++----- histogram.py | 56 ++++++++++++------------- icons/{summary_table.png => table.png} | Bin summary_table.py | 20 ++++----- 5 files changed, 59 insertions(+), 61 deletions(-) rename icons/{summary_table.png => table.png} (100%) diff --git a/common_functions.py b/common_functions.py index 1b61ec3..78900dc 100644 --- a/common_functions.py +++ b/common_functions.py @@ -46,14 +46,14 @@ def simplify_name(string): ) return string.lower().translate(translation_table) -def check_layer_geometry(layer): - """ - Check if the input vector layer is a polygon layer. - """ - if QgsWkbTypes.displayString(layer.wkbType()) not in ['Polygon', 'MultiPolygon']: - iface.messageBar().pushMessage("Erreur", "La zone d'étude fournie n'est pas valide ! Veuillez sélectionner une couche vecteur de type POLYGONE.", level=Qgis.Critical, duration=10) - raise QgsProcessingException("La zone d'étude fournie n'est pas valide ! Veuillez sélectionner une couche vecteur de type POLYGONE.") - return None +# def check_layer_geometry(layer): +# """ +# Check if the input vector layer is a polygon layer. +# """ +# if QgsWkbTypes.displayString(layer.wkbType()) not in ['Polygon', 'MultiPolygon']: +# iface.messageBar().pushMessage("Erreur", "La zone d'étude fournie n'est pas valide ! Veuillez sélectionner une couche vecteur de type POLYGONE.", level=Qgis.Critical, duration=10) +# raise QgsProcessingException("La zone d'étude fournie n'est pas valide ! Veuillez sélectionner une couche vecteur de type POLYGONE.") +# return None def check_layer_is_valid(feedback, layer): """ @@ -73,6 +73,8 @@ def construct_sql_array_polygons(layer): """ # Initialization of the sql array containing the study area's features geometry array_polygons = "array[" + # Retrieve the CRS of the layer + crs = layer.sourceCrs().authid().split(':')[1] # For each entity in the study area... for feature in layer.getFeatures(): # Retrieve the geometry @@ -81,9 +83,9 @@ def construct_sql_array_polygons(layer): geomSingleType = QgsWkbTypes.isSingleType(area.wkbType()) # Increment the sql array if geomSingleType: - array_polygons += "ST_PolygonFromText('{}', 2154), ".format(area.asWkt()) + array_polygons += "ST_transform(ST_PolygonFromText('{}', {}), 2154), ".format(area.asWkt(), crs) else: - array_polygons += "ST_MPolyFromText('{}', 2154), ".format(area.asWkt()) + array_polygons += "ST_transform(ST_MPolyFromText('{}', {}), 2154), ".format(area.asWkt(), crs) # Remove the last "," in the sql array which is useless, and end the array array_polygons = array_polygons[:len(array_polygons)-2] + "]" return array_polygons diff --git a/extract_data.py b/extract_data.py index 26b358c..7aaf399 100644 --- a/extract_data.py +++ b/extract_data.py @@ -36,10 +36,11 @@ QgsProcessingParameterString, QgsProcessingParameterFeatureSource, QgsProcessingOutputVectorLayer, + QgsProcessingParameterDefinition, QgsDataSourceUri, QgsVectorLayer) from processing.tools import postgis -from .common_functions import check_layer_geometry, check_layer_is_valid, construct_sql_array_polygons, load_layer +from .common_functions import check_layer_is_valid, construct_sql_array_polygons, load_layer pluginPath = os.path.dirname(__file__) @@ -55,21 +56,22 @@ class ExtractData(QgsProcessingAlgorithm): STUDY_AREA = 'STUDY_AREA' OUTPUT = 'OUTPUT' OUTPUT_NAME = 'OUTPUT_NAME' + TABLE = 'TABLE' def name(self): return 'ExtractData' def displayName(self): - return 'Extract observation data from study area' + return "Extraction de données d'observation" def icon(self): return QIcon(os.path.join(pluginPath, 'icons', 'extract_data.png')) def groupId(self): - return 'initialisation' + return 'test' def group(self): - return 'Initialisation' + return 'Test' def initAlgorithm(self, config=None): """ @@ -80,7 +82,7 @@ def initAlgorithm(self, config=None): # Data base connection db_param = QgsProcessingParameterString( self.DATABASE, - self.tr('Nom de la connexion à la base de données') + self.tr("1/ Sélectionnez votre connexion à la base de données LPO AuRA") ) db_param.setMetadata( { @@ -93,8 +95,8 @@ def initAlgorithm(self, config=None): self.addParameter( QgsProcessingParameterFeatureSource( self.STUDY_AREA, - self.tr("Zone d'étude"), - [QgsProcessing.TypeVectorAnyGeometry] + self.tr("2/ Sélectionnez votre zone d'étude, à partir de laquelle seront extraites les données d'observations"), + [QgsProcessing.TypeVectorPolygon] ) ) @@ -111,7 +113,7 @@ def initAlgorithm(self, config=None): self.addParameter( QgsProcessingParameterString( self.OUTPUT_NAME, - self.tr("Nom de la couche en sortie"), + self.tr("3/ Définissez un nom pour votre couche en sortie"), self.tr("Données d'observation") ) ) @@ -123,8 +125,6 @@ def processAlgorithm(self, parameters, context, feedback): # Retrieve the input vector layer = study area study_area = self.parameterAsSource(parameters, self.STUDY_AREA, context) - # Check if the study area is a polygon layer - check_layer_geometry(study_area) # Retrieve the output PostGIS layer name and format it layer_name = self.parameterAsString(parameters, self.OUTPUT_NAME, context) ts = datetime.now() @@ -133,7 +133,7 @@ def processAlgorithm(self, parameters, context, feedback): # Construct the sql array containing the study area's features geometry array_polygons = construct_sql_array_polygons(study_area) # Define the "where" clause of the SQL query, aiming to retrieve the output PostGIS layer = biodiversity data - where = "is_valid and ST_within(geom, SY_union({}))".format(array_polygons) + where = "is_valid and ST_within(geom, ST_union({}))".format(array_polygons) # Retrieve the data base connection name connection = self.parameterAsString(parameters, self.DATABASE, context) diff --git a/histogram.py b/histogram.py index 3a93ade..c7f26dd 100644 --- a/histogram.py +++ b/histogram.py @@ -43,7 +43,7 @@ QgsDataSourceUri, QgsVectorLayer) from processing.tools import postgis -from .common_functions import check_layer_geometry, check_layer_is_valid, construct_sql_array_polygons, load_layer +from .common_functions import check_layer_is_valid, construct_sql_array_polygons, load_layer pluginPath = os.path.dirname(__file__) @@ -64,16 +64,16 @@ def name(self): return 'Histogram' def displayName(self): - return 'NE PAS TESTER - Create an histogram' + return 'Etat des connaissances par groupe taxonomique' def icon(self): - return QIcon(os.path.join(pluginPath, 'icons', 'histogram.png')) + return QIcon(os.path.join(pluginPath, 'icons', 'table.png')) def groupId(self): - return 'treatments' + return 'test' def group(self): - return 'Treatments' + return 'Test' def initAlgorithm(self, config=None): """ @@ -84,7 +84,7 @@ def initAlgorithm(self, config=None): # Data base connection db_param = QgsProcessingParameterString( self.DATABASE, - self.tr('Nom de la connexion à la base de données') + self.tr("1/ Sélectionnez votre connexion à la base de données LPO AuRA") ) db_param.setMetadata( { @@ -97,8 +97,8 @@ def initAlgorithm(self, config=None): self.addParameter( QgsProcessingParameterFeatureSource( self.STUDY_AREA, - self.tr("Zone d'étude"), - [QgsProcessing.TypeVectorAnyGeometry] + self.tr("2/ Sélectionnez votre zone d'étude, à partir de laquelle seront extraites les données de l'état des connaissances"), + [QgsProcessing.TypeVectorPolygon] ) ) @@ -115,8 +115,8 @@ def initAlgorithm(self, config=None): self.addParameter( QgsProcessingParameterString( self.OUTPUT_NAME, - self.tr("Nom de la couche en sortie"), - self.tr("Données histogramme") + self.tr("3/ Définissez un nom pour votre couche en sortie"), + self.tr("Etat des connaissances") ) ) @@ -127,8 +127,6 @@ def processAlgorithm(self, parameters, context, feedback): # Retrieve the input vector layer = study area study_area = self.parameterAsSource(parameters, self.STUDY_AREA, context) - # Check if the study area is a polygon layer - check_layer_geometry(study_area) # Retrieve the output PostGIS layer name and format it layer_name = self.parameterAsString(parameters, self.OUTPUT_NAME, context) ts = datetime.now() @@ -142,7 +140,7 @@ def processAlgorithm(self, parameters, context, feedback): # Retrieve the data base connection name connection = self.parameterAsString(parameters, self.DATABASE, context) # Define the SQL query - query = """(SELECT groupe_taxo, COUNT(*) AS nb_observations + query = """(SELECT groupe_taxo, COUNT(*) AS nb_donnees FROM src_lpodatas.observations WHERE {} GROUP BY groupe_taxo @@ -161,22 +159,22 @@ def processAlgorithm(self, parameters, context, feedback): iface.setActiveLayer(layer_histo) iface.showAttributeTable(layer_histo) - plt.rcdefaults() - libel = [feature['groupe_taxo'] for feature in layer_histo.getFeatures()] - feedback.pushInfo('Libellés : {}'.format(libel)) - #X = np.arange(len(libel)) - #feedback.pushInfo('Valeurs en X : {}'.format(X)) - Y = [int(feature['nb_observations']) for feature in layer_histo.getFeatures()] - feedback.pushInfo('Valeurs en Y : {}'.format(Y)) - fig = plt.figure() - ax = fig.add_axes([0, 0, 1, 1]) - # fig, ax = plt.subplots() - ax.bar(libel, Y) - # ax.set_xticks(X) - ax.set_xticklabels(libel) - ax.set_ylabel(u'Nombre d\'observations') - ax.set_title(u'Etat des connaissances par groupes d\'espèces') - plt.show() + # plt.rcdefaults() + # libel = [feature['groupe_taxo'] for feature in layer_histo.getFeatures()] + # feedback.pushInfo('Libellés : {}'.format(libel)) + # #X = np.arange(len(libel)) + # #feedback.pushInfo('Valeurs en X : {}'.format(X)) + # Y = [int(feature['nb_observations']) for feature in layer_histo.getFeatures()] + # feedback.pushInfo('Valeurs en Y : {}'.format(Y)) + # fig = plt.figure() + # ax = fig.add_axes([0, 0, 1, 1]) + # # fig, ax = plt.subplots() + # ax.bar(libel, Y) + # # ax.set_xticks(X) + # ax.set_xticklabels(libel) + # ax.set_ylabel(u'Nombre d\'observations') + # ax.set_title(u'Etat des connaissances par groupes d\'espèces') + # plt.show() return {self.OUTPUT: layer_histo.id()} diff --git a/icons/summary_table.png b/icons/table.png similarity index 100% rename from icons/summary_table.png rename to icons/table.png diff --git a/summary_table.py b/summary_table.py index abe5be2..84c31d2 100644 --- a/summary_table.py +++ b/summary_table.py @@ -41,7 +41,7 @@ QgsDataSourceUri, QgsVectorLayer) from processing.tools import postgis -from .common_functions import simplify_name, check_layer_geometry, check_layer_is_valid, construct_sql_array_polygons, load_layer, execute_sql_queries +from .common_functions import simplify_name, check_layer_is_valid, construct_sql_array_polygons, load_layer, execute_sql_queries pluginPath = os.path.dirname(__file__) @@ -63,16 +63,16 @@ def name(self): return 'SummaryTable' def displayName(self): - return 'Create a summary table per species' + return 'Tableau de synthèse par espèce' def icon(self): - return QIcon(os.path.join(pluginPath, 'icons', 'summary_table.png')) + return QIcon(os.path.join(pluginPath, 'icons', 'table.png')) def groupId(self): - return 'treatments' + return 'test' def group(self): - return 'Treatments' + return 'Test' def initAlgorithm(self, config=None): """ @@ -83,7 +83,7 @@ def initAlgorithm(self, config=None): # Data base connection db_param = QgsProcessingParameterString( self.DATABASE, - self.tr('Nom de la connexion à la base de données') + self.tr("1/ Sélectionnez votre connexion à la base de données LPO AuRA") ) db_param.setMetadata( { @@ -96,8 +96,8 @@ def initAlgorithm(self, config=None): self.addParameter( QgsProcessingParameterFeatureSource( self.STUDY_AREA, - self.tr("Zone d'étude"), - [QgsProcessing.TypeVectorAnyGeometry] + self.tr("2/ Sélectionnez votre zone d'étude, à partir de laquelle seront extraites les données du tableau de synthèse"), + [QgsProcessing.TypeVectorPolygon] ) ) @@ -114,7 +114,7 @@ def initAlgorithm(self, config=None): self.addParameter( QgsProcessingParameterString( self.OUTPUT_NAME, - self.tr("Nom de la couche en sortie"), + self.tr("3/ Définissez un nom pour votre couche en sortie"), self.tr("Tableau synthèse") ) ) @@ -135,8 +135,6 @@ def processAlgorithm(self, parameters, context, feedback): # Retrieve the input vector layer = study area study_area = self.parameterAsSource(parameters, self.STUDY_AREA, context) - # Check if the study area is a polygon layer - check_layer_geometry(study_area) # Retrieve the output PostGIS layer name and format it layer_name = self.parameterAsString(parameters, self.OUTPUT_NAME, context) ts = datetime.now() From 17856667f0f5134726f1178d63a52aae30c5a438 Mon Sep 17 00:00:00 2001 From: eguilley Date: Wed, 6 May 2020 18:43:04 +0200 Subject: [PATCH 11/19] MaJ --- extract_data.py | 22 ++++++++++++++++--- histogram.py | 57 +++++++++++++++++++++++++++++++++++++++++------- summary_table.py | 10 +++++---- 3 files changed, 74 insertions(+), 15 deletions(-) diff --git a/extract_data.py b/extract_data.py index 7aaf399..a93f687 100644 --- a/extract_data.py +++ b/extract_data.py @@ -36,9 +36,10 @@ QgsProcessingParameterString, QgsProcessingParameterFeatureSource, QgsProcessingOutputVectorLayer, - QgsProcessingParameterDefinition, + QgsProcessingParameterFeatureSink, QgsDataSourceUri, - QgsVectorLayer) + QgsVectorLayer, + QgsProcessingException) from processing.tools import postgis from .common_functions import check_layer_is_valid, construct_sql_array_polygons, load_layer @@ -58,6 +59,8 @@ class ExtractData(QgsProcessingAlgorithm): OUTPUT_NAME = 'OUTPUT_NAME' TABLE = 'TABLE' + TARGET_CRS = 'TARGET_CRS' + def name(self): return 'ExtractData' @@ -113,11 +116,19 @@ def initAlgorithm(self, config=None): self.addParameter( QgsProcessingParameterString( self.OUTPUT_NAME, - self.tr("3/ Définissez un nom pour votre couche en sortie"), + self.tr("3/ Définissez un nom pour votre nouvelle couche"), self.tr("Données d'observation") ) ) + # self.addParameter( + # QgsProcessingParameterFeatureSink( + # self.OUTPUT, + # self.tr('4/ Enregistrez votre nouvelle couche...'), + # type=QgsProcessing.TypeVectorAnyGeometry + # ) + # ) + def processAlgorithm(self, parameters, context, feedback): """ Here is where the processing itself takes place. @@ -148,6 +159,11 @@ def processAlgorithm(self, parameters, context, feedback): # Load the PostGIS layer load_layer(context, layer_obs) + # Retrieve sink + # (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, layer_obs.fields(), layer_obs.wkbType(), layer_obs.sourceCrs()) + # if sink is None: + # raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT)) + return {self.OUTPUT: layer_obs.id()} def tr(self, string): diff --git a/histogram.py b/histogram.py index c7f26dd..5a03d00 100644 --- a/histogram.py +++ b/histogram.py @@ -40,10 +40,11 @@ QgsProcessingParameterString, QgsProcessingParameterFeatureSource, QgsProcessingOutputVectorLayer, + QgsProcessingParameterBoolean, QgsDataSourceUri, QgsVectorLayer) from processing.tools import postgis -from .common_functions import check_layer_is_valid, construct_sql_array_polygons, load_layer +from .common_functions import simplify_name, check_layer_is_valid, construct_sql_array_polygons, load_layer, execute_sql_queries pluginPath = os.path.dirname(__file__) @@ -57,6 +58,7 @@ class Histogram(QgsProcessingAlgorithm): # Constants used to refer to parameters and outputs DATABASE = 'DATABASE' STUDY_AREA = 'STUDY_AREA' + ADD_TABLE = 'ADD_TABLE' OUTPUT = 'OUTPUT' OUTPUT_NAME = 'OUTPUT_NAME' @@ -120,6 +122,15 @@ def initAlgorithm(self, config=None): ) ) + # Boolean : True = add the summary table in the DB ; False = don't + self.addParameter( + QgsProcessingParameterBoolean( + self.ADD_TABLE, + self.tr("Enregistrer les données en sortie dans une nouvelle table PostgreSQL"), + False + ) + ) + def processAlgorithm(self, parameters, context, feedback): """ Here is where the processing itself takes place. @@ -139,15 +150,45 @@ def processAlgorithm(self, parameters, context, feedback): # Retrieve the data base connection name connection = self.parameterAsString(parameters, self.DATABASE, context) - # Define the SQL query - query = """(SELECT groupe_taxo, COUNT(*) AS nb_donnees - FROM src_lpodatas.observations - WHERE {} - GROUP BY groupe_taxo - ORDER BY count(*) desc)""".format(where) # URI --> Configures connection to database and the SQL query uri = postgis.uri_from_name(connection) - uri.setDataSource("", query, None, "", "groupe_taxo") + # Retrieve the boolean + add_table = self.parameterAsBool(parameters, self.ADD_TABLE, context) + + if add_table: + # Define the name of the PostGIS summary table which will be created in the DB + table_name = simplify_name(format_name) + # Define the SQL queries + queries = [ + "DROP TABLE if exists {}".format(table_name), + """CREATE TABLE {} AS (SELECT row_number() OVER () AS id, + groupe_taxo, COUNT(*) AS nb_donnees, + COUNT(DISTINCT(source_id_sp)) as nb_especes, + COUNT(DISTINCT(observateur)) as nb_observateurs, + COUNT(DISTINCT("date")) as nb_dates + FROM src_lpodatas.observations + WHERE {} + GROUP BY groupe_taxo + ORDER BY groupe_taxo)""".format(table_name, where), + "ALTER TABLE {} add primary key (id)".format(table_name) + ] + # Execute the SQL queries + execute_sql_queries(context, feedback, connection, queries) + # Format the URI + uri.setDataSource(None, table_name, None, "", "id") + + else: + # Define the SQL query + query = """(SELECT groupe_taxo, COUNT(*) AS nb_donnees, + COUNT(DISTINCT(source_id_sp)) as nb_especes, + COUNT(DISTINCT(observateur)) as nb_observateurs, + COUNT(DISTINCT("date")) as nb_dates + FROM src_lpodatas.observations + WHERE {} + GROUP BY groupe_taxo + ORDER BY groupe_taxo)""".format(where) + # Format the URI + uri.setDataSource("", query, None, "", "groupe_taxo") # Retrieve the output PostGIS layer = histogram data layer_histo = QgsVectorLayer(uri.uri(), format_name, "postgres") diff --git a/summary_table.py b/summary_table.py index 84c31d2..2b03a9b 100644 --- a/summary_table.py +++ b/summary_table.py @@ -160,7 +160,8 @@ def processAlgorithm(self, parameters, context, feedback): "DROP TABLE if exists {}".format(table_name), """CREATE TABLE {} AS (WITH data AS (SELECT source_id_sp, nom_sci AS nom_scientifique, nom_vern AS nom_vernaculaire, groupe_taxo, - COUNT(*) AS nb_donnees, COUNT(distinct(observateur)) AS nb_observateurs, + COUNT(*) AS nb_donnees, COUNT(DISTINCT(observateur)) AS nb_observateurs, + COUNT(DISTINCT("date")) as nb_dates, COALESCE(SUM(CASE WHEN mortalite THEN 1 ELSE 0 END),0) AS nb_mortalite, max(sn.code_nidif) AS max_atlas_code, max(nombre_total) AS nb_individus_max, min (date_an) as premiere_observation, max(date_an) as derniere_observation, @@ -171,7 +172,7 @@ def processAlgorithm(self, parameters, context, feedback): GROUP BY source_id_sp, nom_sci, nom_vern, groupe_taxo), synthese AS (SELECT DISTINCT source_id_sp, nom_scientifique, nom_vernaculaire, groupe_taxo, - nb_donnees, nb_observateurs, nb_mortalite, + nb_donnees, nb_observateurs, nb_dates, nb_mortalite, sn2.statut_nidif, nb_individus_max, premiere_observation, derniere_observation, sources FROM data d @@ -190,7 +191,8 @@ def processAlgorithm(self, parameters, context, feedback): # Define the SQL query query = """(WITH data AS (SELECT source_id_sp, nom_sci AS nom_scientifique, nom_vern AS nom_vernaculaire, groupe_taxo, - COUNT(*) AS nb_donnees, COUNT(distinct(observateur)) AS nb_observateurs, + COUNT(*) AS nb_donnees, COUNT(DISTINCT(observateur)) AS nb_observateurs, + COUNT(DISTINCT("date")) as nb_dates, COALESCE(SUM(CASE WHEN mortalite THEN 1 ELSE 0 END),0) AS nb_mortalite, max(sn.code_nidif) AS max_atlas_code, max(nombre_total) AS nb_individus_max, min (date_an) as premiere_observation, max(date_an) as derniere_observation, @@ -201,7 +203,7 @@ def processAlgorithm(self, parameters, context, feedback): GROUP BY source_id_sp, nom_sci, nom_vern, groupe_taxo), synthese AS (SELECT DISTINCT source_id_sp, nom_scientifique, nom_vernaculaire, groupe_taxo, - nb_donnees, nb_observateurs, nb_mortalite, + nb_donnees, nb_observateurs, nb_dates, nb_mortalite, sn2.statut_nidif, nb_individus_max, premiere_observation, derniere_observation, sources FROM data d From cca764eb1c33532c210a5869ce4283474880561a Mon Sep 17 00:00:00 2001 From: eguilley Date: Thu, 7 May 2020 10:45:02 +0200 Subject: [PATCH 12/19] Try QgsProcessingParameterFeatureSink --- extract_data.py | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/extract_data.py b/extract_data.py index a93f687..4598df9 100644 --- a/extract_data.py +++ b/extract_data.py @@ -104,13 +104,13 @@ def initAlgorithm(self, config=None): ) # Output PostGIS layer = biodiversity data - self.addOutput( - QgsProcessingOutputVectorLayer( - self.OUTPUT, - self.tr('Couche en sortie'), - QgsProcessing.TypeVectorAnyGeometry - ) - ) + # self.addOutput( + # QgsProcessingOutputVectorLayer( + # self.OUTPUT, + # self.tr('Couche en sortie'), + # QgsProcessing.TypeVectorAnyGeometry + # ) + # ) # Output PostGIS layer name self.addParameter( @@ -121,13 +121,14 @@ def initAlgorithm(self, config=None): ) ) - # self.addParameter( - # QgsProcessingParameterFeatureSink( - # self.OUTPUT, - # self.tr('4/ Enregistrez votre nouvelle couche...'), - # type=QgsProcessing.TypeVectorAnyGeometry - # ) - # ) + # Output PostGIS layer = biodiversity data + self.addParameter( + QgsProcessingParameterFeatureSink( + self.OUTPUT, + self.tr('4/ Enregistrez votre nouvelle couche...'), + QgsProcessing.TypeVectorPoint + ) + ) def processAlgorithm(self, parameters, context, feedback): """ @@ -160,11 +161,14 @@ def processAlgorithm(self, parameters, context, feedback): load_layer(context, layer_obs) # Retrieve sink - # (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, layer_obs.fields(), layer_obs.wkbType(), layer_obs.sourceCrs()) - # if sink is None: - # raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT)) - - return {self.OUTPUT: layer_obs.id()} + (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, layer_obs.fields(), layer_obs.wkbType(), layer_obs.sourceCrs()) + if sink is None: + raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT)) + for feature in layer_obs.getFeatures(): + sink.addFeature(feature) + + #return {self.OUTPUT: layer_obs.id()} + return {self.OUTPUT: dest_id} def tr(self, string): return QCoreApplication.translate('Processing', string) From b318d174150053d731e1cebe53b2bd0e8d3cee19 Mon Sep 17 00:00:00 2001 From: eguilley Date: Thu, 7 May 2020 18:26:21 +0200 Subject: [PATCH 13/19] MaJ --- extract_data.py | 55 ++++++++++++++++++++++++++++--------------------- histogram.py | 15 +++++++++++--- 2 files changed, 43 insertions(+), 27 deletions(-) diff --git a/extract_data.py b/extract_data.py index 4598df9..9bce433 100644 --- a/extract_data.py +++ b/extract_data.py @@ -30,7 +30,7 @@ from datetime import datetime from qgis.PyQt.QtGui import QIcon -from qgis.PyQt.QtCore import QCoreApplication +from qgis.PyQt.QtCore import QCoreApplication, QVariant from qgis.core import (QgsProcessing, QgsProcessingAlgorithm, QgsProcessingParameterString, @@ -39,7 +39,7 @@ QgsProcessingParameterFeatureSink, QgsDataSourceUri, QgsVectorLayer, - QgsProcessingException) + QgsField) from processing.tools import postgis from .common_functions import check_layer_is_valid, construct_sql_array_polygons, load_layer @@ -104,13 +104,13 @@ def initAlgorithm(self, config=None): ) # Output PostGIS layer = biodiversity data - # self.addOutput( - # QgsProcessingOutputVectorLayer( - # self.OUTPUT, - # self.tr('Couche en sortie'), - # QgsProcessing.TypeVectorAnyGeometry - # ) - # ) + self.addOutput( + QgsProcessingOutputVectorLayer( + self.OUTPUT, + self.tr('Couche en sortie'), + QgsProcessing.TypeVectorAnyGeometry + ) + ) # Output PostGIS layer name self.addParameter( @@ -122,13 +122,13 @@ def initAlgorithm(self, config=None): ) # Output PostGIS layer = biodiversity data - self.addParameter( - QgsProcessingParameterFeatureSink( - self.OUTPUT, - self.tr('4/ Enregistrez votre nouvelle couche...'), - QgsProcessing.TypeVectorPoint - ) - ) + # self.addParameter( + # QgsProcessingParameterFeatureSink( + # self.OUTPUT, + # self.tr('4/ Enregistrez votre nouvelle couche...'), + # QgsProcessing.TypeVectorPoint + # ) + # ) def processAlgorithm(self, parameters, context, feedback): """ @@ -161,14 +161,21 @@ def processAlgorithm(self, parameters, context, feedback): load_layer(context, layer_obs) # Retrieve sink - (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, layer_obs.fields(), layer_obs.wkbType(), layer_obs.sourceCrs()) - if sink is None: - raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT)) - for feature in layer_obs.getFeatures(): - sink.addFeature(feature) - - #return {self.OUTPUT: layer_obs.id()} - return {self.OUTPUT: dest_id} + # try: + # (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, layer_obs.fields(), layer_obs.wkbType(), layer_obs.sourceCrs()) + # except Exception as e: + # raise e + + # try: + # if sink is None: + # raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT)) + # for feature in layer_obs.getFeatures(): + # sink.addFeature(feature) + # except Exception as e: + # raise e + + #return {self.OUTPUT: dest_id} + return {self.OUTPUT: layer_obs.id()} def tr(self, string): return QCoreApplication.translate('Processing', string) diff --git a/histogram.py b/histogram.py index 5a03d00..0868b61 100644 --- a/histogram.py +++ b/histogram.py @@ -31,8 +31,9 @@ from qgis.PyQt.QtGui import QIcon from qgis.utils import iface -import matplotlib.pyplot as plt -import numpy as np +# import plotly as plt +# import plotly.graph_objs as go +# import numpy as np from qgis.PyQt.QtCore import QCoreApplication from qgis.core import (QgsProcessing, @@ -179,7 +180,7 @@ def processAlgorithm(self, parameters, context, feedback): else: # Define the SQL query - query = """(SELECT groupe_taxo, COUNT(*) AS nb_donnees, + query = """(SELECT groupe_taxo, COUNT(*) AS nb_donnees, COUNT(DISTINCT(source_id_sp)) as nb_especes, COUNT(DISTINCT(observateur)) as nb_observateurs, COUNT(DISTINCT("date")) as nb_dates @@ -200,6 +201,14 @@ def processAlgorithm(self, parameters, context, feedback): iface.setActiveLayer(layer_histo) iface.showAttributeTable(layer_histo) + # x_var = [feature['groupe_taxo'] for feature in layer_histo.getFeatures()] + # y_var = [int(feature['nb_donnees']) for feature in layer_histo.getFeatures()] + # data = [go.Bar(x=x_var, y=y_var)] + # plt.offline.plot(data, filename="/home/eguilley/Téléchargements/histogram-test.html", auto_open=True) + # fig = go.Figure(data=data) + # fig.show() + # fig.write_image("/home/eguilley/Téléchargements/histogram-test.png") + # plt.rcdefaults() # libel = [feature['groupe_taxo'] for feature in layer_histo.getFeatures()] # feedback.pushInfo('Libellés : {}'.format(libel)) From de5c7bfeb5e3749b2e0fb515537003a329031a1a Mon Sep 17 00:00:00 2001 From: eguilley Date: Thu, 7 May 2020 22:44:28 +0200 Subject: [PATCH 14/19] MaJ --- extract_data.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/extract_data.py b/extract_data.py index 9bce433..a9df918 100644 --- a/extract_data.py +++ b/extract_data.py @@ -160,6 +160,22 @@ def processAlgorithm(self, parameters, context, feedback): # Load the PostGIS layer load_layer(context, layer_obs) + # Create the new fields for the sink + fields = layer_obs.fields() + new_fields = layer_obs.fields() + new_fields.clear() + for f in fields: + if f.name() == 'comportement': + new_fields.append(QgsField('comportement', QVariant., "text")) + feedback.pushInfo('OK') + elif f.name() == 'details': + new_fields.append(QgsField('details', QVariant.TextFormat, "text")) + feedback.pushInfo('OK') + else: + new_fields.append(f) + for i,field in enumerate(new_fields): + feedback.pushInfo('Elt : {}- {} {}'.format(i, field.name(), field.typeName())) + # Retrieve sink # try: # (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, layer_obs.fields(), layer_obs.wkbType(), layer_obs.sourceCrs()) From d2df8afb4415283f17d14a242be09b5157b1b75b Mon Sep 17 00:00:00 2001 From: eguilley Date: Mon, 11 May 2020 09:43:44 +0200 Subject: [PATCH 15/19] Correction QgsProcessingParameterFeatureSink --- extract_data.py | 73 +++++++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 39 deletions(-) diff --git a/extract_data.py b/extract_data.py index a9df918..cdc9932 100644 --- a/extract_data.py +++ b/extract_data.py @@ -39,7 +39,8 @@ QgsProcessingParameterFeatureSink, QgsDataSourceUri, QgsVectorLayer, - QgsField) + QgsField, + QgsProcessingException) from processing.tools import postgis from .common_functions import check_layer_is_valid, construct_sql_array_polygons, load_layer @@ -57,9 +58,6 @@ class ExtractData(QgsProcessingAlgorithm): STUDY_AREA = 'STUDY_AREA' OUTPUT = 'OUTPUT' OUTPUT_NAME = 'OUTPUT_NAME' - TABLE = 'TABLE' - - TARGET_CRS = 'TARGET_CRS' def name(self): return 'ExtractData' @@ -104,13 +102,13 @@ def initAlgorithm(self, config=None): ) # Output PostGIS layer = biodiversity data - self.addOutput( - QgsProcessingOutputVectorLayer( - self.OUTPUT, - self.tr('Couche en sortie'), - QgsProcessing.TypeVectorAnyGeometry - ) - ) + # self.addOutput( + # QgsProcessingOutputVectorLayer( + # self.OUTPUT, + # self.tr('Couche en sortie'), + # QgsProcessing.TypeVectorAnyGeometry + # ) + # ) # Output PostGIS layer name self.addParameter( @@ -122,13 +120,13 @@ def initAlgorithm(self, config=None): ) # Output PostGIS layer = biodiversity data - # self.addParameter( - # QgsProcessingParameterFeatureSink( - # self.OUTPUT, - # self.tr('4/ Enregistrez votre nouvelle couche...'), - # QgsProcessing.TypeVectorPoint - # ) - # ) + self.addParameter( + QgsProcessingParameterFeatureSink( + self.OUTPUT, + self.tr('4/ Enregistrez votre nouvelle couche...'), + QgsProcessing.TypeVectorPoint + ) + ) def processAlgorithm(self, parameters, context, feedback): """ @@ -166,32 +164,29 @@ def processAlgorithm(self, parameters, context, feedback): new_fields.clear() for f in fields: if f.name() == 'comportement': - new_fields.append(QgsField('comportement', QVariant., "text")) - feedback.pushInfo('OK') + new_fields.append(QgsField('comportement', QVariant.String, "str")) elif f.name() == 'details': - new_fields.append(QgsField('details', QVariant.TextFormat, "text")) - feedback.pushInfo('OK') + new_fields.append(QgsField('details', QVariant.String, "str")) else: new_fields.append(f) - for i,field in enumerate(new_fields): - feedback.pushInfo('Elt : {}- {} {}'.format(i, field.name(), field.typeName())) + # for i,field in enumerate(new_fields): + # feedback.pushInfo('Elt : {}- {} {}'.format(i, field.name(), field.typeName())) # Retrieve sink - # try: - # (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, layer_obs.fields(), layer_obs.wkbType(), layer_obs.sourceCrs()) - # except Exception as e: - # raise e - - # try: - # if sink is None: - # raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT)) - # for feature in layer_obs.getFeatures(): - # sink.addFeature(feature) - # except Exception as e: - # raise e - - #return {self.OUTPUT: dest_id} - return {self.OUTPUT: layer_obs.id()} + try: + (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, new_fields, layer_obs.wkbType(), layer_obs.sourceCrs()) + except Exception as e: + raise e + try: + if sink is None: + raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT)) + for feature in layer_obs.getFeatures(): + sink.addFeature(feature) + except Exception as e: + raise e + + return {self.OUTPUT: dest_id} + #return {self.OUTPUT: layer_obs.id()} def tr(self, string): return QCoreApplication.translate('Processing', string) From f0ff069cee7e97b55389b9a1f8908f7326824c8e Mon Sep 17 00:00:00 2001 From: eguilley Date: Mon, 11 May 2020 16:15:41 +0200 Subject: [PATCH 16/19] Ajout fonction commune format_layer_export() --- common_functions.py | 21 +++++++++++++- extract_data.py | 70 +++++++++++++++++---------------------------- 2 files changed, 47 insertions(+), 44 deletions(-) diff --git a/common_functions.py b/common_functions.py index 78900dc..8fdba06 100644 --- a/common_functions.py +++ b/common_functions.py @@ -29,7 +29,9 @@ from qgis.utils import iface from qgis.gui import QgsMessageBar +from qgis.PyQt.QtCore import QVariant from qgis.core import (QgsWkbTypes, + QgsField, QgsProcessingException, Qgis) import processing @@ -123,4 +125,21 @@ def execute_sql_queries(context, feedback, connection, queries): feedback=feedback ) feedback.pushInfo('Requête SQL exécutée avec succès !') - return None \ No newline at end of file + return None + +def format_layer_export(layer): + """ + Create new valid fields for the sink. + """ + old_fields = layer.fields() + new_fields = layer.fields() + new_fields.clear() + invalid_formats = ["_text", "jsonb"] + for field in old_fields: + if field.typeName() in invalid_formats: + new_fields.append(QgsField(field.name(), QVariant.String, "str")) + else: + new_fields.append(field) + # for i,field in enumerate(new_fields): + # feedback.pushInfo('Elt : {}- {} {}'.format(i, field.name(), field.typeName())) + return new_fields diff --git a/extract_data.py b/extract_data.py index cdc9932..452c21f 100644 --- a/extract_data.py +++ b/extract_data.py @@ -40,9 +40,11 @@ QgsDataSourceUri, QgsVectorLayer, QgsField, - QgsProcessingException) + QgsProcessingUtils, + QgsProcessingException, + QgsProject) from processing.tools import postgis -from .common_functions import check_layer_is_valid, construct_sql_array_polygons, load_layer +from .common_functions import check_layer_is_valid, construct_sql_array_polygons, load_layer, format_layer_export pluginPath = os.path.dirname(__file__) @@ -58,6 +60,7 @@ class ExtractData(QgsProcessingAlgorithm): STUDY_AREA = 'STUDY_AREA' OUTPUT = 'OUTPUT' OUTPUT_NAME = 'OUTPUT_NAME' + dest_id = None def name(self): return 'ExtractData' @@ -101,15 +104,6 @@ def initAlgorithm(self, config=None): ) ) - # Output PostGIS layer = biodiversity data - # self.addOutput( - # QgsProcessingOutputVectorLayer( - # self.OUTPUT, - # self.tr('Couche en sortie'), - # QgsProcessing.TypeVectorAnyGeometry - # ) - # ) - # Output PostGIS layer name self.addParameter( QgsProcessingParameterString( @@ -123,8 +117,11 @@ def initAlgorithm(self, config=None): self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, - self.tr('4/ Enregistrez votre nouvelle couche...'), - QgsProcessing.TypeVectorPoint + self.tr('4/ Si nécessaire, enregistrez votre nouvelle couche (vous pouvez aussi ignorer cette étape)'), + QgsProcessing.TypeVectorPoint, + None, + True, + False ) ) @@ -155,38 +152,25 @@ def processAlgorithm(self, parameters, context, feedback): layer_obs = QgsVectorLayer(uri.uri(), format_name, "postgres") # Check if the PostGIS layer is valid check_layer_is_valid(feedback, layer_obs) - # Load the PostGIS layer - load_layer(context, layer_obs) - - # Create the new fields for the sink - fields = layer_obs.fields() - new_fields = layer_obs.fields() - new_fields.clear() - for f in fields: - if f.name() == 'comportement': - new_fields.append(QgsField('comportement', QVariant.String, "str")) - elif f.name() == 'details': - new_fields.append(QgsField('details', QVariant.String, "str")) - else: - new_fields.append(f) - # for i,field in enumerate(new_fields): - # feedback.pushInfo('Elt : {}- {} {}'.format(i, field.name(), field.typeName())) - - # Retrieve sink - try: - (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, new_fields, layer_obs.wkbType(), layer_obs.sourceCrs()) - except Exception as e: - raise e - try: - if sink is None: - raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT)) + + # Create new valid fields for the sink + new_fields = format_layer_export(layer_obs) + # Retrieve the sink for the export + (sink, self.dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, new_fields, layer_obs.wkbType(), layer_obs.sourceCrs()) + if sink is None: + # Load the PostGIS layer and return it + load_layer(context, layer_obs) + return {self.OUTPUT: layer_obs.id()} + else: + # Fill the sink and return it for feature in layer_obs.getFeatures(): sink.addFeature(feature) - except Exception as e: - raise e - - return {self.OUTPUT: dest_id} - #return {self.OUTPUT: layer_obs.id()} + return {self.OUTPUT: self.dest_id} + + #def postProcessAlgorithm(self, context, feedback): + # processed_layer = QgsProcessingUtils.mapLayerFromString(self.dest_id, context) + # feedback.pushInfo('Processed_layer : {}'.format(processed_layer)) + #return {self.OUTPUT: self.dest_id} def tr(self, string): return QCoreApplication.translate('Processing', string) From 701340028bfbdabe53c663b565adbfeb65eee743 Mon Sep 17 00:00:00 2001 From: eguilley Date: Tue, 12 May 2020 14:53:31 +0200 Subject: [PATCH 17/19] push before release --- common_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common_functions.py b/common_functions.py index 8fdba06..b25c1df 100644 --- a/common_functions.py +++ b/common_functions.py @@ -100,7 +100,7 @@ def load_layer(context, layer): plugin_lpo_group = root.findGroup('Résultats plugin LPO') if not plugin_lpo_group: plugin_lpo_group = root.insertGroup(0, 'Résultats plugin LPO') - context.project().addMapLayers([layer], False) + context.project().addMapLayer(layer, False) plugin_lpo_group.addLayer(layer) ### Variant # context.temporaryLayerStore().addMapLayer(layer) @@ -111,7 +111,7 @@ def load_layer(context, layer): def execute_sql_queries(context, feedback, connection, queries): """ - Execute severals sql queries. + Execute several sql queries. """ for query in queries: processing.run( From a617274e66b0234eb78cc1a354924edf351cc53b Mon Sep 17 00:00:00 2001 From: lpofredc Date: Mon, 18 May 2020 11:28:46 +0200 Subject: [PATCH 18/19] Add README and CHANGELOG --- CHANGELOG.md | 8 ++++++++ README.md | 16 ++++++++++++++++ README.txt | 26 -------------------------- 3 files changed, 24 insertions(+), 26 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 README.md delete mode 100644 README.txt diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..912c1a3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# v0.0.1 + +Pemière pre-release avec un premier lot de fonctions: +* Extraction de données à partir d'une couche de zonages +* Création d'un tableau de synthèse par taxons à partir d'une couche de zonages +* Création d'un graphique de synthèse d'état des connaissances par groupes taxonomiques à partir d'une couche de zonages + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..99c7881 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# Scripts de processing framework de la LPO AuRA + +Ce plugin ajoute à QGIS des scripts d'exploitation des données naturalistes de +la [LPO Auvergne-Rhône-Alpes](https://auvergne-rhone-alpes.lpo.fr/) à QGIS. + +## Licence + +## Equipe + +* @eguilley (LPO Auvergne-Rhône-Alpes) +* @lpofredc (@lpofredc - LPO Auvergne-Rhône-Alpes) +* @jgirardclaudon (@lpojgc LPO Auvergne-Rhône-Alpes) + +![logoLPO AuRA](https://raw.githubusercontent.com/lpoaura/biodivsport-widget/master/images/LPO_AuRA_l250px.png) + + diff --git a/README.txt b/README.txt deleted file mode 100644 index 4de51bf..0000000 --- a/README.txt +++ /dev/null @@ -1,26 +0,0 @@ -Plugin Builder Results - -Your plugin ScriptsLPO was created in: - /home/eguilley/Dev/plugin_qgis/scripts_lpo/scripts_lpo - -Your QGIS plugin directory is located at: - /home/eguilley/.local/share/QGIS/QGIS3/profiles/default/python/plugins - -What's Next: - - * Copy the entire directory containing your new plugin to the QGIS plugin - directory - - * Run the tests (``make test``) - - * Test the plugin by enabling it in the QGIS plugin manager - - * Customize it by editing the implementation file: ``scripts_lpo.py`` - - * You can use the Makefile to compile your Ui and resource files when - you make changes. This requires GNU make (gmake) - -For more information, see the PyQGIS Developer Cookbook at: -http://www.qgis.org/pyqgis-cookbook/index.html - -(C) 2011-2018 GeoApt LLC - geoapt.com From 69d7fb05574b4fd15fe10c328e1d5b531e74cd65 Mon Sep 17 00:00:00 2001 From: lpofredc Date: Mon, 18 May 2020 11:29:59 +0200 Subject: [PATCH 19/19] add VERSION --- VERSION | 1 + 1 file changed, 1 insertion(+) create mode 100644 VERSION diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..95e94cd --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +v0.0.1 \ No newline at end of file