1 (edited by javdenech Today 12:48:55)

Topic: script auto numerotation des bornes

Script pour l'auto numérotation des bornes

Re: script auto numerotation des bornes

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Programme pour autonuméroter les bornes dans les fichiers QElectroTech (.qet)
Version finale qui fonctionne correctement
"""

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import xml.etree.ElementTree as ET
import os
from collections import OrderedDict

class QETBorneNumbering:
    def __init__(self, root):
        self.root = root
        self.root.title("Autonumérotation des bornes QElectroTech")
        self.root.geometry("800x600")
        
        # Variables
        self.file_path = None
        self.xml_root = None
        self.bornes_trouvees = []
        self.borne_selectionnee = None
        self.use_wire_num = tk.BooleanVar(value=False)
        
        self.create_widgets()
    
    def create_widgets(self):
        """Crée l'interface graphique"""
        # Frame principale
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # Configuration du poids pour le redimensionnement
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)
        main_frame.columnconfigure(1, weight=1)
        main_frame.rowconfigure(3, weight=1)
        
        # Bouton pour ouvrir le fichier
        btn_open = ttk.Button(main_frame, text="Ouvrir un fichier .qet", command=self.open_file)
        btn_open.grid(row=0, column=0, columnspan=2, pady=10, sticky=tk.W+tk.E)
        
        # Label pour afficher le fichier ouvert
        self.label_file = ttk.Label(main_frame, text="Aucun fichier sélectionné", foreground="gray")
        self.label_file.grid(row=1, column=0, columnspan=2, sticky=tk.W)
        
        # Frame pour les bornes trouvées
        frame_bornes = ttk.LabelFrame(main_frame, text="Bornes trouvées", padding="5")
        frame_bornes.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=10)
        frame_bornes.columnconfigure(0, weight=1)
        
        # Listbox pour les bornes
        self.listbox_bornes = tk.Listbox(frame_bornes, height=6)
        self.listbox_bornes.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        self.listbox_bornes.bind('<<ListboxSelect>>', self.on_borne_select)
        
        # Scrollbar pour la listbox
        scrollbar = ttk.Scrollbar(frame_bornes, orient="vertical", command=self.listbox_bornes.yview)
        scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
        self.listbox_bornes.configure(yscrollcommand=scrollbar.set)
        
        # Frame pour les boutons d'action
        frame_actions = ttk.LabelFrame(main_frame, text="Actions", padding="5")
        frame_actions.grid(row=3, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
        frame_actions.columnconfigure(0, weight=1)
        
        # Bouton d'autonumérotation
        self.btn_numeroter = ttk.Button(frame_actions, text="Numéroter les bornes sélectionnées", 
                                       command=self.numeroter_bornes, state="disabled")
        self.btn_numeroter.grid(row=0, column=0, pady=5, sticky=tk.W+tk.E)

        # Checkbox pour le mode de numérotation (Numéro de fil)
        self.chk_wire = ttk.Checkbutton(frame_actions, text="Utiliser n° fil (Label_Fil)", 
                                       variable=self.use_wire_num)
        self.chk_wire.grid(row=1, column=0, pady=5, sticky=tk.W)
        
        # Bouton pour sauvegarder
        self.btn_save = ttk.Button(frame_actions, text="Sauvegarder le fichier", 
                                  command=self.save_file, state="disabled")
        self.btn_save.grid(row=1, column=0, pady=5, sticky=tk.W+tk.E)
        self.btn_save.grid(row=2, column=0, pady=5, sticky=tk.W+tk.E)
        
        # Zone de statut
        self.status_var = tk.StringVar(value="Prêt")
        status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN)
        status_bar.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(10, 0))
    
    def open_file(self):
        """Ouvre un fichier .qet et analyse les bornes"""
        file_path = filedialog.askopenfilename(
            title="Sélectionner un fichier QElectroTech",
            filetypes=[("Fichiers QElectroTech", "*.qet"), ("Tous les fichiers", "*.*")]
        )
        
        if file_path:
            try:
                self.file_path = file_path
                self.label_file.config(text=f"Fichier: {os.path.basename(file_path)}")
                
                # Lecture et analyse du fichier XML
                self.analyze_file()
                
                # Réinitialiser l'état de modification pour le nouveau fichier
                if hasattr(self, '_already_modified'):
                    del self._already_modified
                
                self.status_var.set(f"Fichier chargé: {len(self.bornes_trouvees)} bornes trouvées")
                
            except Exception as e:
                messagebox.showerror("Erreur", f"Impossible d'ouvrir le fichier:\n{str(e)}")
                self.status_var.set("Erreur lors de l'ouverture du fichier")
    
    def analyze_file(self):
        """Analyse le fichier XML pour trouver les bornes"""
        try:
            # Parser le fichier XML
            tree = ET.parse(self.file_path)
            self.xml_root = tree.getroot()
            
            # Extraire les bornes
            self.bornes_trouvees = self.extract_bornes()
            
            # Mettre à jour la listbox (sans doublons)
            self.update_listbox()
            
        except ET.ParseError as e:
            raise Exception(f"Erreur de parsing XML: {str(e)}")
        except Exception as e:
            raise Exception(f"Erreur lors de l'analyse: {str(e)}")
    
    def extract_bornes(self):
        """Extrait toutes les bornes du fichier XML"""
        bornes = []
        
        # Parcourir tous les diagrammes pour trouver les folios
        for diagram in self.xml_root.iter('diagram'):
            folio = diagram.get('folio', '1')
            
            # Parcourir les éléments dans ce diagramme
            for element in diagram.iter('element'):
                x = float(element.get('x', '0'))
                y = float(element.get('y', '0'))
                
                # Vérifier si c'est une borne (commence par X)
                for child in element:
                    if child.tag == 'elementInformations':
                        for info in child:
                            if info.tag == 'elementInformation':
                                name = info.get('name', '')
                                if name == 'label':
                                    text = info.text
                                    if text and text.upper().startswith('X'):
                                        bornes.append({
                                            'element': element,
                                            'label': text,
                                            'position': (x, y),
                                            'folio': folio
                                        })
                                        break
        
        return bornes
    
    def get_borne_base(self, label):
        """Extrait la base du label (tronque après ':')"""
        if ':' in label:
            return label.split(':')[0]
        return label
    
    def update_listbox(self):
        """Met à jour la listbox avec les bornes uniques"""
        self.listbox_bornes.delete(0, tk.END)
        
        # Extraire les bases de labels uniques (tronquer avant ':')
        bases_uniques = list(OrderedDict.fromkeys([self.get_borne_base(b['label']) for b in self.bornes_trouvees]))
        
        for label in sorted(bases_uniques):
            self.listbox_bornes.insert(tk.END, label)
        
        # Activer le bouton de numérotation si des bornes sont trouvées
        if bases_uniques:
            self.btn_numeroter.config(state="normal")
        else:
            self.btn_numeroter.config(state="disabled")
    
    def on_borne_select(self, event):
        """Gère la sélection d'une borne dans la listbox"""
        selection = self.listbox_bornes.curselection()
        if selection:
            index = selection[0]
            self.borne_selectionnee = self.listbox_bornes.get(index)
            self.status_var.set(f"Borne sélectionnée: {self.borne_selectionnee}")
    
    def get_connected_wire_text(self, diagram, element_id):
        """Trouve le texte du conducteur connecté à un élément donné"""
        if not element_id:
            return ""
        # Chercher dans les conducteurs du diagramme
        for conductor in diagram.iter('conductor'):
            # Vérification directe des attributs element1 et element2 (Format QET standard)
            # Le numéro de fil est dans l'attribut 'num'
            if conductor.get('element1') == element_id or conductor.get('element2') == element_id:
                text = conductor.get('num', '')
                if text and text.strip() and text.strip() != "_":
                    return text.strip()
            
            # Compatibilité avec d'autres formats potentiels (points de connexion)
            for point in conductor.iter('point'):
                if point.get('element') == element_id:
                    # Vérifier l'attribut text ou num
                    text = conductor.get('text', '')
                    if not text:
                        text = conductor.get('num', '')
                    
                    if text and text.strip() and text.strip() != "_":
                        return text.strip()
        return ""

    def numeroter_bornes(self):
        """Numérote les bornes sélectionnées dans leur ordre d'apparition"""
        if not self.borne_selectionnee:
            messagebox.showwarning("Avertissement", "Veuillez sélectionner une borne à numéroter")
            return
        
        try:
            # Si c'est la première numérotation, recharger le fichier XML
            if not hasattr(self, '_already_modified'):
                tree = ET.parse(self.file_path)
                self.xml_root = tree.getroot()
                self._already_modified = True
            # Sinon, utiliser l'arbre XML déjà modifié
            
            # Filtrer les bornes correspondant à la base sélectionnée
            bornes_a_numeroter = []
            for diagram in self.xml_root.iter('diagram'):
                folio = diagram.get('folio', '1')
                for element in diagram.iter('element'):
                    x = float(element.get('x', '0'))
                    y = float(element.get('y', '0'))
                    
                    for child in element:
                        if child.tag == 'elementInformations':
                            for info in child:
                                if info.tag == 'elementInformation':
                                    name = info.get('name', '')
                                    if name == 'label':
                                        text = info.text
                                        if text and self.get_borne_base(text) == self.borne_selectionnee:
                                            bornes_a_numeroter.append({
                                                'element': element,
                                                'label': text,
                                                'position': (x, y),
                                                'folio': folio,
                                                'diagram': diagram # Nécessaire pour trouver les fils
                                            })
                                            break
            
            if not bornes_a_numeroter:
                messagebox.showwarning("Avertissement", "Aucune borne trouvée pour ce label")
                return
            
            def natural_sort_key(s):
                """Fonction utilitaire pour trier les folios numériquement si possible"""
                import re
                return [int(text) if text.isdigit() else text.lower()
                        for text in re.split('([0-9]+)', s)]

            # Trier par folio puis par position (gauche à droite)
            # On utilise natural_sort_key pour le folio pour éviter que "10" soit avant "2"
            bornes_triees = sorted(bornes_a_numeroter, 
                                 key=lambda x: (natural_sort_key(x['folio']), x['position'][0], x['position'][1]))
            
            # Numéroter les bornes avec format X1:00, X1:01, etc.
            for i, borne in enumerate(bornes_triees):
                num_seq = i + 1
                if self.use_wire_num.get():
                    # Mode concaténation : X1:1_102 (Bornier:Index_Fil)
                    # QET utilise l'attribut 'uuid' pour l'identifiant unique
                    element_id = borne['element'].get('uuid')
                    if not element_id:
                        element_id = borne['element'].get('id')
                        
                    wire_text = self.get_connected_wire_text(borne['diagram'], element_id)
                    suffixe = wire_text if wire_text else "?"
                    nouveau_label = f"{self.borne_selectionnee}:{num_seq}_{suffixe}"
                else:
                    # Mode séquentiel classique
                    nouveau_label = f"{self.borne_selectionnee}:{num_seq}"
                
                # Modifier elementInformation
                for child in borne['element']:
                    if child.tag == 'elementInformations':
                        for info in child:
                            if info.tag == 'elementInformation':
                                name = info.get('name', '')
                                if name == 'label':
                                    info.text = nouveau_label
                    
                    # Modifier dynamic_text
                    elif child.tag == 'dynamic_texts':
                        for dynamic_text in child:
                            if dynamic_text.tag == 'dynamic_elmt_text':
                                for sub_child in dynamic_text:
                                    if sub_child.tag == 'info_name' and sub_child.text == 'label':
                                        for text_elem in dynamic_text:
                                            if text_elem.tag == 'text':
                                                text_elem.text = nouveau_label
            
            self.status_var.set(f"{len(bornes_triees)} bornes {self.borne_selectionnee} numérotées avec succès")
            self.btn_save.config(state="normal")
            
            messagebox.showinfo("Succès", f"{len(bornes_triees)} bornes {self.borne_selectionnee} ont été numérotées")
            
        except Exception as e:
            messagebox.showerror("Erreur", f"Erreur lors de la numérotation:\n{str(e)}")
    
    def save_file(self):
        """Sauvegarde le fichier modifié"""
        if not self.xml_root:
            messagebox.showwarning("Avertissement", "Aucun fichier à sauvegarder")
            return
        
        try:
            # Demander où sauvegarder
            save_path = filedialog.asksaveasfilename(
                title="Sauvegarder le fichier",
                defaultextension=".qet",
                filetypes=[("Fichiers QElectroTech", "*.qet"), ("Tous les fichiers", "*.*")],
                initialfile=os.path.basename(self.file_path)
            )
            
            if save_path:
                # Créer une copie de l'arbre XML pour la sauvegarde
                tree = ET.ElementTree(self.xml_root)
                
                # Configuration pour un beau formatage
                ET.indent(tree, space="  ", level=0)
                
                # Sauvegarder le fichier
                tree.write(save_path, encoding='utf-8', xml_declaration=True)
                
                self.status_var.set(f"Fichier sauvegardé: {os.path.basename(save_path)}")
                messagebox.showinfo("Succès", "Fichier sauvegardé avec succès")
                
        except Exception as e:
            messagebox.showerror("Erreur", f"Erreur lors de la sauvegarde:\n{str(e)}")

def main():
    """Fonction principale"""
    root = tk.Tk()
    app = QETBorneNumbering(root)
    root.mainloop()

if __name__ == "__main__":
    main()