Calculer une adresse métrique dans QGIS avec PyQGIS.

Depuis quelques temps, je travaille sur une extension pour QGIS qui permettra de gérer des systèmes d’adressage postal complexe. L’idée est de partir de la BAN puis de travailler avec les collectivités à l’amélioration de leur système en ayant un outil fluide et complet qui réponde bien à tous les cas de figure.

Le système est basé sur deux types d’objets principaux qui sont:

  • Les routes nommées sous forme de lignes : le réseaux de voies nommées qui supporteront les adresses
  • Les adresses sous formes de lignes bi-point (un point sur la route correspondante, un point sur l’entrée de la maison)

Une belle route avec ses belles adresses!

Le problème?

L’idée est de pouvoir définir pour chaque route nommée comment est défini son système d’adressage. Les choix sont les suivants : métrique, séquentiel (voir ici pour les définitions) ou pseudo-métrique (métrique avec un loupé sur le début de la route… ça arrive plus souvent qu’on ne le croit….).

Une des fonctionnalités attendue du plugin est de calculer automatiquement l’adresse d’une maison lorsque le type d’adresse est métrique. La fonction se base donc sur deux paramètres : la route avec son point de départ et l’adresse représentée par son bi-point. Il y a une notion de parité (pair à droite, impair à gauche).

Adressage métrique, tout un art!

Le calcul d’une adresse doit se faire de la manière suivante:

  1. calculer la distance séparant le début de la rue et l’adresse (point du bi-point sur la route) sous la forme d’un entier (facile!)
  2. Suivant le côté de la route (droite ou gauche) sur lequel est l’habitation, recalculer l’adresse car les maisons côté droit doivent avoir une adresse paire et les maisons côté gauche doivent avoir un numéro impair… (euh…)
  3. mettre à jour la valeur « nom de la rue » et « numéro » du bi-point de l’adresse (facile!)

Vous l’aurez compris, la difficulté de l’exercice est de définir la parité d’une adresse mathématiquement. En effet, les cas de figure sont incroyablement variés:

Un exemple un peu moche de rue complexe

Une solution pourrait consister à comparer les valeurs en X/Y du point de la route et du point de la maison pour le bi-point de l’adresse… Cependant, un tel algorithme impliquerait un bloc de condition monstrueux et complexe à maintenir/débuger…

En train de réfléchir à la comparaison des X/Y….

La solution?

Finalement, après avoir perdu quelques cheveux… j’ai réfléchis à une approche basé sur les azimuths…

Pour rappel  (wikipédia):

L’azimut (parfois orthographié azimuth) est l’angle dans le plan horizontal entre la direction d’un objet et une direction de référence. Cette référence peut être le Nord géographique ou magnétique. L’azimut est mesuré depuis le nord en degrés de 0° (inclus) à 360° (exclu) dans le sens rétrograde (sens des aiguilles d’une montre) : ainsi l’Est est au 90°, le Sud au 180° et l’Ouest au 270°.

Une adresse est représentée par une ligne composée de deux points, donc on peut facilement en récupérer l’azimuth. Cette ligne vient s’accrocher à un segment de notre route dont on peut également récupérer l’azimuth. On a donc deux valeurs comparable en degrés par rapport au nord: azimuth de l’adresse (azimuth_adresse) et azimuth du segment de la route sur lequel est accroché l’adresse (azimuth_route)(<– relire lentement si ce n’est pas clair). Bon, En quoi, on récupère la parité avec ça…?

Voila l’astuce :

La solution basée sur les azimuths!

En partant de azimuth_adresse et en tournant dans le sens horaire, si l’adresse est à gauche de la route, je vais finir par être égal à azimuth_route. En revanche, si l’adresse est à droite en réalisant la même opération, je vais d’abord être égal à azimuth_route – 180!! Ainsi donc une simple différence permet de déterminer la parité –> mieux.

Voici ce que ça me donne une fois traduis en PyQgis pour le plugin…

adress_number_calculator
from math import degrees

def convert_180_to_360(azimuth):
    """
    Convertis les azimuths [-180;180] en [0;360]
    """
    if azimuth < 0: 
        return 360 - abs(azimuth)
    else:
        return azimuth

def diff_clockwise(azimuth_cible, azimuth_source):
    """
    Retourne l'angle dans le sens des aiguilles 
    d'une montre entre deux azimuth
    """
    a = azimuth_cible - azimuth_source
    a =  ((a + 180) % 360) - 180
    if a < 0 :
        convert_180_to_360(a)
    return a
    
def verify_side_from_azimuth(azimuth_route, azimuth_adresse):
    """
    Fonction de controle de la parite entre deux lineaire
    en assumant que : 
    - cote gauche = impair
    - cote droit = pair
       
    La fonction doit tenir compte de l'orientation de 
    ligne (donc du sens dans lequel on regarde).
    """
    if azimuth_route < 180:  
        azimuth_oppose = azimuth_route + 180
    elif azimuth_route > 180:
        azimuth_oppose = azimuth_route - 180 
    else: 
        azimuth_oppose = 360 
    
    if diff_clockwise(azimuth_oppose, azimuth_adresse) < diff_clockwise(azimuth_route, azimuth_adresse):
        orientation = 'impair'
    else:
       orientation = 'pair'
    return orientation

def numero_adresse_calculator(ligne_route, ligne_adresse):
    """
    Fonction qui prend deux lignes en entrée:
    - ligne_route : QgsFeature linéaire repésentant la route
                   associée
    - ligne_adresse : QgsFeature bipoint linéraire avec premier
                   vertex sur la maison et deuxième sur la route 
                   représentant l'adresse
        
    La fonction renvois l'adresse métrique de la ligne 
    en tenant compte du pair/impair
    """
    #on recupere les geometrie
    geom_route = ligne_route.geometry()
    pt_road = ligne_adresse.geometry().vertexAt(0)  #point
    pt_house = ligne_adresse.geometry().vertexAt(1) #point
    
    #on calcule l'adresse sans tenir compte de la parite
    pt_adresse = QgsGeometry.fromPoint(pt_road)
    numero_adresse_brut = int(geom_route.lineLocatePoint(pt_adresse))
    
    #on construit l'azimuth de la route en le calculant avec les vertex
    #environnant (n-1, n+1) pour eviter que les lignes poser sur des 
    #vertex ne soient mal traitee
    
    point = geom_route.interpolate(numero_adresse_brut).asPoint()
    dist, nearest_v = geom_route.closestVertexWithContext(point)
    max_vertex = geom_route.geometry().numPoints()
    
    if QgsGeometry.fromPoint(geom_route.vertexAt(nearest_v)).buffer(0.1,5).intersects(pt_adresse):
        return None, numero_adresse_brut
    else:
        previous_iv = nearest_v - 1 if nearest_v != 0 else 0 #vertex precedent sauf si le vertex le plus proche est le premier
        next_iv = nearest_v + 1 if nearest_v != max_vertex-1 else max_vertex -1 #vertex suivant sauf si le vertex le plus proche est le dernier

        #recuperation des vertex en fonction de leur index
        previous_v = geom_route.vertexAt(previous_iv) 
        next_v = geom_route.vertexAt(next_iv)

        #calcul de l'azimuth [-180;180] entre les deux vertex qu'on converti en ]0;360]
        azimuth_route = convert_180_to_360(previous_v.azimuth(next_v))

        # azimuth de la ligne d'adresse en degree (0->360). 
        azimuth_adresse = convert_180_to_360(pt_road.azimuth(pt_house))
    
        #controle de la parité par rapport au cote et modification du numero en fonction (+1)
        parity = verify_side_from_azimuth(azimuth_route, azimuth_adresse)  
        if numero_adresse_brut%2 == 0 and parity == 'impair':
            numero_adresse = numero_adresse_brut + 1
        elif numero_adresse_brut%2 != 0 and parity == 'pair':
            numero_adresse = numero_adresse_brut + 1
        else:
            numero_adresse = numero_adresse_brut
        return parity, numero_adresse

Implémentation réelle

Finalement, on a opté pour une solution à base de Triggers dans la BDD pour des raisons de cohérence et de découplage. L’adresse est recalculée en dynamique lorsqu’on modifie l’objet (Logique –> BDD, interface –> Plugin (python/Qt/QGIS)). Mais le trigger est basé sur cette logique.

Limite du Système…

Il y a néanmoins un cas de figure où ça ne marche pas… Si le point de l’adresse est situé pile sur un vertex de la route et que la ligne d’adresse est un chouilla tangente, il y a risque d’échec… C’est du au fait qu’il n’y a pas vraiment de segment de route associé à l’adresse, le système prend donc un azimuth de route inconsistant… Pas encore trouver de solution à ça en python… Mais en jouant sur la topologie, on peut s’assurer qu’on n’est jamais accroché sur un noeud et là le système est fiable pour ce que j’en ai vu et testé.

Là où ça ne marche pas…

N’hésitez pas à me faire part d’autres méthodes pour résoudre ça ou de remarques sur le code! Je suis preneur des critiques ;)!

Répondre

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.