banniere

Le portail francophone de la géomatique


Toujours pas inscrit ? Mot de passe oublié ?
Nom d'utilisateur    Mot de passe              Toujours pas inscrit ?   Mot de passe oublié ?

Annonce

Rencontres QGIS 2025

L'appel à participation est ouvert jusqu'au 19 janvier 2025!

#1 Sat 20 October 2018 00:16

F.Duval
Participant occasionnel
Date d'inscription: 3 Jan 2012
Messages: 23

QGIS 3: Console Python - conditions complexes pour remplir un champ

Bonjour à la communauté,

Après des années à inspecter le site sans jamais poster, cette fois je me lance !
J'ai fait une formation Python d'une semaine il y a quelques mois, en partant de zéro, donc j'ai un niveau très débutant.
J'aimerais, s'il vous plait, que quelqu'un puisse m'éclairer un peu.

Mon projet est d'obtenir l'information 'MATRICULE(s) AMONT(s)' pour chaque regard d'un réseau d’assainissement. Il peut y avoir plusieurs regards Amont pour un seul regard, car venant de plusieurs directions et il me faudrait tous leur matricule. Uniquement le regard précédent, pas en remontant tout le réseau.

Après divers traitement j'arrive à ce shape de point 'joinspat' :
[img]https://imageshack.com/a/img923/6408/sk6yIc.jpg[/img]

Voici mon script, les Print() du début n'ont pas trop d'intérêt mais je l'ai modifié petit à petit et il peut peut être servir à d'autres très débutants.
Pour moi, en tâtonnant, il m'a aidé à comprendre certaines choses hyper basiques.

Code:

#j'ai préalablement sélectionné la couche 'joinspat'
layer=iface.activeLayer()    

tableur = layer.dataProvider()

for champs in tableur.fields():
    print(champs.name())
#(((la console me retourne les 3 shapes de mon projet
#snapline
#joinspat
#Regard_Amont
 
#la table attributaire de référence est celle 'joinspat'
tableur = layer.dataProvider()

for champs in tableur.fields():
    print(champs.name())
#la console me retourne les champs de 'joinspat'
#DESCRIPTEU
#MATRICULE
#COMM1
#COMM2
#COMM3
#COMM4
#ZR
#MAT_AMONT

#je crée des raccourcis pour appeler mes champs
DESCRIPTEU = tableur.fields()[0]
MATRICULE = tableur.fields()[1]
ZR = tableur.fields()[2]
COMM1 = tableur.fields()[3]
COMM2 = tableur.fields()[4]
COMM3 = tableur.fields()[5]
COMM4 = tableur.fields()[6]
MAT_AMONT = tableur.fields()[7]

#activer le mode éditeur
layer.startEditing()

#itération sur les enregistrements de la couche : A PARTIR DE LA CE N'EST PLUS DU CODE MAIS CA DONNE UNE IDEE
for regard in layer.getFeatures():
    if DESCRIPTEU de la ligne1 contient la chaine de caractère de COMM1 d'une ligne n (avec itération sur cette ligne n)
    elif ZR de ligne1 < ZR ligne n
    then écrire MATRICULE de ligne n dans MAT_AMONT

# Création de la nouvelle valeur à inscrire dans la table
attribut = { ....... }

# changer l'attribut de MAT_AMONT dans la couche
layer.dataProvider().changeAttributeValues({ .... : attribut })

#enregistrer et quitter mode editeur
layer.commitChanges()

Je vais essayer d'expliquer le plus clairement possible le traitement ,qui me semble complexe, que je souhaite faire.

Pour chaque objet x, lorsque la chaîne de 3 caractères de COMM1 se retrouve dans le DESCPRIPTEU (qui peut avoir plus de 3 caractères) d'un autre objet 'n' du même shape.(Il se retrouvera forcément dans le DESCRPITEU du même objet mais cela ne m'intéresse pas)
Alors je voudrais comparer le ZR avec le ZR de 'n' et si ZR < ZR'n' ça veut dire que cet objet 'n' est un regard Amont.
Dans ce cas je veux que le MATRICULE de cet objet 'n' soit écrit dans MAT_AMONT de mon objet x

En sachant que :

-ZR est du string et que si les deux ZR à comparer peuvent être passer en float (24.66 par exemple) je peux faire la comparaison, si un des deux est du texte ('Recouvert' par exemple) je laisse un blanc et passe à la ligne suivante. Il doit y avoir un str(....) ou float(...) qui apparaît quelque part..

-Il faudrait faire le même traitement sur COMM2, COMM3 et COMM4 : des boucles dans des boucles ?

-Potentiellement un regard peut avoir jusqu'à 3 ou 4 regards amont (d'où les COMM1 à 4) et j'aimerais enregistrer plusieurs matricules dans le champ MAT_AMONT, et non pas que le dernier écrase les précédents. Je pense que ce n'est pas le plus difficile; j'imagine qu'il faut faire 4 variables de résultats (une pour chaque COMM*) qui peuvent être NULL ou non et écrire dans MAT_AMONT la concaténation des 4, avec un - entre les variables.

Je prends toutes les infos,corrections, propositions... Même si cela ne résout qu'une infime partie du script c'est déjà génial de m'aider un peu ; donc n'hésitez pas!
Je reste à votre dispo.
Merci

F.DUVAL

Hors ligne

 

#2 Mon 22 October 2018 10:39

dominique.lys
Participant assidu
Date d'inscription: 5 Oct 2006
Messages: 473
Site web

Re: QGIS 3: Console Python - conditions complexes pour remplir un champ

Bonjour et bienvenue sur le forum,

En prenant connaissance de votre problème je me demandais naïvement s'il ça serait plus simple de numéroté vos regards de l'amont vers l'aval plutôt que de stocker les identifiants amonts dans une chaine concaténée. Bien-sûr cela implique d'avoir une numérotation distincte pour chaque ligne donc autant de champs que de lignes ce qui j'imagine peut être conséquent.

Je me demandais également si le critère d'altitude était absolument fiable, ne peut ont pas avoir deux regards successifs avec la même altitude ? Si c'est le cas l'altitude ne pourra pas être un critère suffisant pour déterminer le regard amont.

Quelques ressources par rapport à Python dont vous avez peut être connaissance)
- les méthodes de bases Python : https://www.programiz.com/python-progra … s/built-in
- la documentation de l'API pyQGIS https://qgis.org/pyqgis/3.0/index.html
- le cookbook : https://docs.qgis.org/2.18/en/docs/pyqg … _cookbook/

Attention il y a des changement important dans l'API Python entre QGIS 2.18 et QGIS 3

Voici quelques éléments pour vous aider à avancer dans votre réflexion :

- il n'est pas nécessaire d'aller récupérer les champs avec la fonction fields(), l'objet QgsField que vous récupérez avec cette fonction à pour vocation de manipuler les champs (nom, type, précisons) mais pas d’accéder aux valeurs des attributs. Pour accéder aux valeurs quand vous itérez sur les points avec getFeatures() vous pouvez directement lire un champs à la manière d'un dictionnaire Python : feature['MATRICULE']. Noter par ailleurs que par convention les noms de variable en majuscule sont réservés aux constantes.

- vous pouvez vous passer de vos champs COMM1 COMM2 ..., l'information se trouve déjà dans votre champs DESCRIPTEU et vous pouvez traiter directement cette valeur dans votre code. Python offre de nombreuses fonction pour manipuler les chaines de caractères (https://www.programiz.com/python-progra … ods/string). La fonction split() vous permet notamment de découper une chaine en fonction d'un caractère donné. Ainsi '2B7-2B8'.split('-') vous retournera directement une liste de la forme ['2B7', '2B8'].

- il faut anticiper la structure des données dans votre programme, c'est à dire le type de conteneur/de variable qui sera utilisé pour représenter et organiser au mieux les données selon votre problématique. Dans votre cas vous pouvez par exemple vous appuyer sur les dictionnaires Python qui permettent d'élaborer des structures de type clé/valeur. On peut imaginer construire un dictionnaire pour chaque point dont la clé sera constituée des numéros des lignes concernées par le point, et les valeurs seraient le matricule du regard amont ou mieux un second dictionnaire dont la clé est le matricule et la valeur l'altitude, exemple : {'2B7':{48300:20}, '2B8':{48400:25}}

- l'inverse de la fonction split() et la fonction join() qui permet d'assembler les éléments d'une liste avec un caractère, la syntaxe peut prêter à confusion car il s'agit d'une méthode de la classe string et non de la classe list: '-'.join(['2B7', '2B8']) retournera la chaine '2B7-2B8'. Vous pourrez utiliser la méthode join() sur une liste ou un dictionnaire pour construire la chaine de caractère de votre champs MAT_AMONT.

- la fonction float() vous permettra de convertir l'information d'altitude en un nombre décimal : z = float(feat['ZR'])
Évidement convertir du texte en float n'est pas possible et vous aurez une erreur. Il serait plus logique dans l’organisation de vos données que le champs ZR soit dés le départ de type float et que l'information 'RECOUVREMENT' qui renseigne sur un état et non l'altitude soit matérialisé ailleurs. Néanmoins vous pouvez traiter le problème par un bloc try except qui permet de ne pas planter si la conversion échoue mais de prévoir ce qu'il faut faire en cas d'échec, exemple :

Code:

        try:
            z2 = float(feat1['ZR'])
        except ValueError:
            #code à executer en cas d'échec

- trouver les regards amonts nécessite de comparer chaque regard avec tous les autres afin d'évaluer certains critères : le regard est-il sur la même ligne, son altitude est-elle supérieure... Ainsi, comme vous le pressentez, il faut utiliser des boucles imbriquées:

Code:

    for feat1 in layer.getFeatures():
    
        #récupérer ici les attributs de feat1, tester des conditions
        #pour déterminer si l'on continue ou pas
    
        for feat2 in layer.getFeatures():
            
            #idem, on récupère les attributs de feat2 et on teste des conditions
            #puis on compare

Le problème ici est que comparer tous le monde avec tous le monde est lent. Avec 1000 points cela fait déjà 1000² itérations. Il faut donc établir le maximum de critère pour éviter le plus en amont possible toute comparaison inutile. Par exemple si feat1 n'a pas de valeur d'altitude la comparaison ne sera pas possible et ce point n'a donc pas à être comparer avec les autres. Le mot clé continue permet dans un boucle de passer directement à l'itération suivante et vous sera très utile dans ce cas de figure, exemple :

Code:

    for feat1 in layer.getFeatures():
    
        try:
            z2 = float(feat1['ZR'])
        except ValueError:
            continue #en cas d'échec de la conversion on passe à l'itération suivante de la première boucle
    
        #dés lors la seconde boucle n'est pas executée inutilement, on gagne du temps de traitement
        for feat2 in layer.getFeatures():

Si vous avez beaucoup de points, il peut être intéressant de d'établir d'autres critères afin que la seconde boucle qui représente les points à tester ne s’exécute pas sur l'intégralité de la couche mais une sélection de points. On pourrait pas exemple présélectionner tous les points partageant un même identifiant de ligne ou bien encore utiliser un critère spatiale si l'on sait par exemple qu'un regard amont ne peut pas être éloigné de plus de 500 mètres, alors on ne testera que les regards se trouvant dans un rayon de 500m.


Voilà un peu de grain à moudre ! Bon courage.

Hors ligne

 

#3 Fri 16 November 2018 12:47

F.Duval
Participant occasionnel
Date d'inscription: 3 Jan 2012
Messages: 23

Re: QGIS 3: Console Python - conditions complexes pour remplir un champ

Bonjour
Message court qui appel une suite :

Tout d'abord je vous remercie beaucoup d'avoir pris le temps d'étudier et de répondre à ma question


Pour répondre à vos questions :

- Les identifiants sont générés aléatoirement lors de l'export depuis Autocad et donc je ne peux pas y faire grand chose (je crois, je ne connais quasiment pas autocad) mais c'est déjà bien d'avoir un ID unique différent pour chaque tronçon.

- oui les altitudes sont fiables, on ne peut avoir deux regards à suivre avec la même altitude.
Merci pou rl'explication avec la fonction Fields()

Je n'ai pas encore eu le temps de m'y remettre mais merci pour ce grain que vous m'avez donné à moudre.
Je ne manquerais pas de venir poster l'avancé, voir la finalité de mon script ici...

Hors ligne

 

Pied de page des forums

Powered by FluxBB