Browse Source

Bases de l'interface et de la structure ok

omassot 7 years ago
parent
commit
d46b793377

+ 2 - 2
__init__.py

@@ -25,8 +25,8 @@
 import os
 import sys
 
-sys.path.insert(0, os.path.join(__file__, os.pardir))
-sys.path.insert(0, os.path.join(os.path.join(__file__, os.pardir), "lib"))
+sys.path.insert(0, os.path.normpath(os.path.join(__file__, os.pardir)))
+sys.path.insert(0, os.path.normpath(os.path.join(os.path.join(__file__, os.pardir), "lib")))
 
 # noinspection PyPep8Naming
 def classFactory(iface):  # pylint: disable=invalid-name

+ 20 - 1
core/constants.py

@@ -11,4 +11,23 @@ RSCDIR = MAIN / "ui" / "rsc"
 LOGDIR = Path("%appdata%").expandvars() / "mncheck"
 LOGDIR.mkdir_p()
 
-LOGCONF = MAIN / "core" / "logging.yaml"
+LOGCONF = MAIN / "core" / "logging.yaml"
+
+
+
+GEOM_UNKNOWN = 0
+GEOM_POINT = 1
+GEOM_LINE = 2
+GEOM_POLYGON = 3
+GEOM_MULTIPOINT = 4
+GEOM_MULTILINE = 5
+GEOM_MULTIPOLYGON = 6
+
+GEOM_NAMES = {0: "(AUCUN)", 
+               1: "POINT", 
+               2: "LIGNE", 
+               3: "POLYGONE", 
+               4: "MULTI-POINT", 
+               5:"MULTI-LIGNE", 
+               6:"MULTI-POLYGONE",
+               }

+ 7 - 7
core/logging.yaml

@@ -11,11 +11,11 @@ formatters:
         format: "%(message)s"
         
 handlers:
-    console:
-        class: logging.StreamHandler
-        level: INFO
-        formatter: message_only
-        stream: ext://sys.stdout
+#    console:
+#        class: logging.StreamHandler
+#        level: INFO
+#        formatter: message_only
+#        stream: ext://sys.stdout
     file:
         class: logging.handlers.RotatingFileHandler
         level: DEBUG
@@ -28,10 +28,10 @@ handlers:
 loggers:
     mncheck:
         level: DEBUG
-        handlers: [console, file]
+        handlers: [file]
         propagate: no
 
 root:
     level: DEBUG
-    handlers: [console]
+    handlers: []
     propagate: yes

+ 0 - 630
core/validation.py

@@ -1,630 +0,0 @@
-'''
-
-
-    @author: olivier.massot, sept. 2018
-'''
-import time
-import zipfile
-
-from path import Path, TempDir
-
-from core import gis_
-from core.cerberus_extend import CerberusErrorHandler, \
-    _translate_messages, ExtendedValidator
-from schemas.common import SRID
-
-        
-class ValidatorInterruption(BaseException): 
-    pass
-
-class Checkpoint():
-    def __init__(self, name, valid=True):
-        self.name = name
-        self.valid = valid
-
-
-###########    MODELES    ################
-
-class BaseModel():
-    filename = ""
-    pk = ""
-    schema = {}
-    def __init__(self, **kwargs):
-        self.__dict__.update(kwargs)
-        
-class BaseGeoModel(gis_.Feature):
-    filename = ""
-    pk = ""
-    geom_type = 0
-    bounding_box = (0,0,1,1)
-    schema = {}
-    
-    def __init__(self, feature):
-        self.__dict__.update(feature.__dict__)
-
-
-###########    ERREURS DE VALIDATION     ################
-
-VALIDATION_ERROR_LEVELS = {10: "MINEURE", 20: "AVERTISSEMENT", 30: "ERREUR", 40: "CRITIQUE"}
-MINOR = 10
-WARNING = 20
-ERROR = 30
-CRITICAL = 40
-
-class BaseValidationError():
-    order_ = 0
-    name = "Erreur"
-    level = ERROR
-    help = ""
-    def __init__(self, message, filename="", field=""):
-        self.message = message
-        self.filename = filename
-        self.field = field
-        
-    def __repr__(self):
-        return " - ".join(filter(None, [self.name, self.filename, self.field, self.message]))
-
-# Erreurs dans le chargement des fichiers
-class InputError(BaseValidationError):
-    order_ = 0
-    level = CRITICAL
-    name = "Erreur de chargement"
-
-class MissingFile(InputError):
-    order_ = 1
-    name = "Fichier Manquant"
-    
-class UnreadableFile(InputError):
-    order_ = 2
-    name = "Fichier Illisible"
-
-class WrongSrid(InputError):
-    order_ = 3
-    name = "Mauvais SRID"
-
-### Erreurs dans la structure des données
-class StructureError(BaseValidationError):
-    order_ = 10
-    name = "Erreur de structure"
-    level = ERROR
-    
-class GeomTypeError(StructureError):
-    order_ = 12
-    name = "Type de géométrie invalide"
-    level = CRITICAL
-    
-class BoundingBoxError(StructureError):
-    order_ = 11
-    name = "Coordonnées hors de la zone autorisée"
-    
-class InvalidGeometry(StructureError):
-    order_ = 13
-    name = "Géométrie invalide"
-    
-class DataError(StructureError):
-    order_ = 14
-    name = "Erreur de format"
-
-# Erreurs dans le contenu, erreurs métiers
-class TechnicalValidationError(BaseValidationError):
-    order_ = 20
-    level = ERROR
-    name = "Erreur technique"
-
-class UniqueError(TechnicalValidationError):
-    order_ = 21
-    name = "Doublons dans le champs"
-
-class RelationError(TechnicalValidationError):
-    order_ = 22
-    level = CRITICAL
-    name = "Un objet lié n'existe pas"
-
-class DuplicatedGeom(TechnicalValidationError):
-    order_ = 23
-    name = "Doublon graphique"
-
-class MissingItem(TechnicalValidationError):
-    order_ = 24
-    name = "Elément manquant"
-
-class DimensionError(TechnicalValidationError):
-    order_ = 25
-    name = "Elément de dimension"
-
-class PositionError(TechnicalValidationError):
-    order_ = 26
-    name = "Erreur de positionnement"
-    
-###########    VALIDATION    ################
-
-class BaseValidator():
-    schema_name = ""
-    models = {}
-    dataset = {}
-    
-    def __init__(self):
-        self.valid = True
-        self.checkpoints = []
-        self.errors = []
-        self._current_checkpoint_valid = True
-        self.dt = 0
-    
-    def checkpoint(self, title):
-        self.checkpoints.append(Checkpoint(title, self._current_checkpoint_valid))
-        self._current_checkpoint_valid = True
-        if self.errors:
-            self.valid = False
-            if self.critical_happened():
-                raise ValidatorInterruption()
-             
-    def critical_happened(self):
-        return any([err.level == CRITICAL for err in self.errors])
-             
-    def log_error(self, validation_error):
-        self._current_checkpoint_valid = False
-        self.errors.append(validation_error)
-         
-    @classmethod
-    def submit(cls, subject):    
-        """ prends un dossier ou une archive en entrée et vérifie son contenu  """
-        subject = Path(subject)
-        
-        if subject.isfile():
-            with TempDir() as dirname:
-                zip_ref = zipfile.ZipFile(subject, 'r')
-                zip_ref.extractall(dirname)
-                zip_ref.close()
-                if Path(dirname / subject.stem).isdir(): # cas où l'archive contient un dossier qui lui-même contient les fichiers
-                    dirname /= subject.stem
-                return cls._submit_folder(dirname)
-            
-        elif subject.isdir():
-            return cls._submit_folder(subject)
-        else:
-            raise FileNotFoundError(f"Impossible de trouver le fichier ou répertoire: {subject}")
-
-    @classmethod
-    def _submit_folder(cls, folder):
-        
-        validator = cls()
-        t0 = time.time()
-        try:
-            validator.validate(folder)
-        except ValidatorInterruption:
-            pass
-        validator.dt = time.time() - t0
-        
-        report = validator.build_report(validator.schema_name, folder.name)
-        return report
-    
-    def validate(self, folder):
-        
-        # Chargement des données en mémoire
-        self._load_files(folder)
-        self.checkpoint("Chargement des données")
-        
-        # Controle la structure des données (champs, formats et types)
-        self._structure_validation()
-        self.checkpoint("Contrôle de la structure des données")
-        
-        # Validation technique
-        try:
-            self._technical_validation()
-            self.checkpoint("Validation Métier")
-        except ValidatorInterruption:
-            raise
-        except:
-            self.checkpoint("Validation Métier [interrompu]")
-    
-    def _load_files(self, folder):
-        """ Charge les données du fichier et les associe à un modèle.
-        Attention: pas de contrôle de format ou de validité à ce niveau!  """
-        raise NotImplementedError()
-    
-    def _structure_validation(self):
-        
-        for model in self.models:
-            v = ExtendedValidator(model.schema, purge_unknown=True, error_handler=CerberusErrorHandler, require_all=True)
-            
-            for item in self.dataset[model]:
-
-                v.validate(item.__dict__)
-            
-                for field, verrors in v.errors.items():
-                    for err in verrors:
-                        self.log_error(DataError(_translate_messages(err), filename=model.filename, field=field))
-    
-    @classmethod
-    def _technical_validation(cls):
-        raise NotImplementedError()
-    
-    
-    def build_report(self, schema, filename):
-        report = {}
-        report["schema"] = schema
-        report["filename"] = filename
-        report["exec_time"] = "{:.3g} s.".format(self.dt)
-        report["checkpoints"] = [{"name": chk.name, "valid": chk.valid} for chk in self.checkpoints]
-        
-        report["errors"] = {}
-        
-        for err in self.errors:
-            if not err.name in report["errors"]:
-                report["errors"][err.name] = {"help": err.help, "order_": err.order_, "list": []}
-            
-            err_report = {"filename": err.filename or "-",
-                          "field": err.field or "-",
-                          "message": err.message}
-            if err_report not in report["errors"][err.name]["list"]:
-                report["errors"][err.name]["list"].append(err_report)
-            
-        return report
-    
-        
-class NetgeoValidator(BaseValidator):
-    
-    def _load_files(self, folder):
-        
-        for model in self.models:
-            filename = model.filename
-            path_ = Path(folder) / filename
-            
-            if not path_.isfile():
-                self.log_error(MissingFile("Fichier manquant: '{}'".format(filename)))
-                continue
-            
-            self.dataset[model] = []
-            try:
-                
-                ds = gis_.Datasource(path_)
-                layer = ds.layer
-                
-                if layer.srid != SRID:
-                    self.log_error(WrongSrid("Mauvaise projection: {} (attendu: {})".format(layer.srid, SRID)))
-                
-                for feature in layer:
-                    
-                    item = model(feature)
-                    
-                    self.dataset[model].append(item)
-                        
-            except IOError:
-                self.log_error(UnreadableFile("Fichier illisible: {}".format(path_.name)))
-    
-    def _structure_validation(self):
-        
-        for model in self.models:
-            v = ExtendedValidator(model.schema, purge_unknown=True, error_handler=CerberusErrorHandler, require_all=True)
-            xmin, ymin, xmax, ymax = model.bounding_box
-            
-            for item in self.dataset[model]:
-
-                # geom type
-                if item.geom_type != model.geom_type:
-                    self.log_error(GeomTypeError("Type de géométrie invalide: {} (attendu: {})".format(item.geom_name, gis_.GEOM_NAMES[model.geom_type]), filename=model.filename, field="geom"))
-
-                # bounding box
-                x1, y1, x2, y2 = item.bounding_box
-                if any(x < xmin or x > xmax for x in (x1, x2)) or \
-                   any(y < ymin or y > ymax for y in (y1, y2)):
-                    self.log_error(BoundingBoxError("Situé hors de l'emprise autorisée", filename=model.filename, field="geom"))
-
-                v.validate(item.__dict__)
-               
-                for field, verrors in v.errors.items():
-                    for err in verrors:
-                        self.log_error(DataError(_translate_messages(err), filename=model.filename, field=field))
-    
-#     def _technical_validation(self):
-#         
-#         # construction des index
-#         arteres = self.dataset[Artere]
-#         cables = self.dataset[Cable]
-#         tranchees = self.dataset[Tranchee]
-#         
-#         noeuds = {}
-#         for noeud in self.dataset[Noeud]:
-#             if not noeud.NO_NOM in noeuds:
-#                 noeuds[noeud.NO_NOM] = noeud
-#             else:
-#                 self.log_error(UniqueError("Doublons dans le champs: {}".format(noeud), filename=Noeud.filename, field="NO_NOM"))
-#         
-#         equipements = {}
-#         for equipement in self.dataset[Equipement]:
-#             if not equipement.EQ_NOM in equipements:
-#                 equipements[equipement.EQ_NOM] = equipement
-#             else:
-#                 self.log_error(UniqueError("Doublons dans le champs: {}".format(equipement), filename=Equipement.filename, field="EQ_NOM"))
-#                 
-#         zapbos = {}
-#         for zapbo in self.dataset[Zapbo]:
-#             if not zapbo.ID_ZAPBO in zapbos:
-#                 zapbos[zapbo.ID_ZAPBO] = zapbo
-#             else:
-#                 self.log_error(UniqueError("Doublons dans le champs: {}".format(zapbo), filename=Zapbo.filename, field="ID_ZAPBO"))
-#         
-#         # contrôle de la validité des géométries
-#         for artere in arteres:
-#             if not artere.valid:
-#                 self.log_error(InvalidGeometry("Géométrie invalide: {}".format(artere), filename=Artere.filename, field="geom"))
-#         for tranchee in tranchees:
-#             if not tranchee.valid:
-#                 self.log_error(InvalidGeometry("Géométrie invalide: {}".format(tranchee), filename=Tranchee.filename, field="geom"))
-#         for cable in cables:
-#             if not "baguette" in cable.CA_COMMENT.lower() and not cable.valid:
-#                 self.log_error(InvalidGeometry("Géométrie invalide: {}".format(cable), filename=Cable.filename, field="geom"))
-#         for noeud in noeuds.values():
-#             if not noeud.valid:
-#                 self.log_error(InvalidGeometry("Géométrie invalide: {}".format(noeud), filename=Noeud.filename, field="geom"))
-#         for equipement in equipements.values():
-#             if not equipement.valid:
-#                 self.log_error(InvalidGeometry("Géométrie invalide: {}".format(equipement), filename=Equipement.filename, field="geom"))
-#         for zapbo in zapbos.values():
-#             if not zapbo.valid:
-#                 self.log_error(InvalidGeometry("Géométrie invalide: {}".format(zapbo), filename=Zapbo.filename, field="geom"))
-#         
-#         # rattachement les noeuds aux artères     
-#         for artere in arteres:
-#             try:
-#                 artere.noeud_a = noeuds[artere.AR_NOEUD_A]
-#             except KeyError:
-#                 artere.noeud_a = None
-#                 self.log_error(RelationError("Le noeud '{}' n'existe pas".format(artere.AR_NOEUD_A), filename=Artere.filename, field="AR_NOEUD_A"))
-#                 
-#             try:
-#                 artere.noeud_b = noeuds[artere.AR_NOEUD_B]
-#             except KeyError:
-#                 artere.noeud_b = None
-#                 self.log_error(RelationError("Le noeud '{}' n'existe pas".format(artere.AR_NOEUD_B), filename=Artere.filename, field="AR_NOEUD_A"))
-#         
-#         # rattachement des equipements aux cables
-#         for cable in cables:
-#             try:
-#                 cable.equipement_a = equipements[cable.CA_EQ_A]
-#             except KeyError:
-#                 cable.equipement_a = None
-#                 self.log_error(RelationError("L'équipement '{}' n'existe pas".format(cable.CA_EQ_A), filename=Cable.filename, field="CA_EQ_A"))
-#                 
-#             try:
-#                 cable.equipement_b = equipements[cable.CA_EQ_B]
-#             except KeyError:
-#                 cable.equipement_b = None
-#                 self.log_error(RelationError("L'équipement '{}' n'existe pas".format(cable.CA_EQ_B), filename=Cable.filename, field="CA_EQ_B"))
-# 
-#         # rattachement des equipements aux noeuds
-#         for equipement in equipements.values():
-#             try:
-#                 equipement.noeud = noeuds[equipement.EQ_NOM_NOE]
-#             except KeyError:
-#                 equipement.noeud = None
-#                 self.log_error(RelationError("Le noeud '{}' n'existe pas".format(equipement.EQ_NOM_NOE, equipement.EQ_NOM), filename=Equipement.filename, field="EQ_NOM_NOE"))
-# 
-#         # verifie que tous les equipements sont l'equipement B d'au moins un cable
-#         equipements_b = [cable.CA_EQ_B for cable in cables]
-#         for eq_id in equipements:
-#             if equipements[eq_id].EQ_TYPE == "BAI":
-#                 continue
-#             if not eq_id in equipements_b:
-#                 self.log_error(RelationError("L'equipement '{}' n'est l'équipement B d'aucun cable".format(eq_id), filename=Equipement.filename, field="EQ_NOM"))
-# 
-#         # controle des doublons graphiques
-#         for i, tranchee in enumerate(tranchees):
-#             for other in tranchees[i+1:]:
-#                 if tranchee.geom == other.geom:
-#                     self.log_error(DuplicatedGeom("Une entité graphique est dupliquée".format(tranchee), filename=Tranchee.filename, field="geom"))
-#                     
-#         for i, artere in enumerate(arteres):
-#             for other in arteres[i+1:]:
-#                 if artere.geom == other.geom:
-#                     self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('{}')".format(artere), filename=Artere.filename, field="geom"))
-# 
-#         for i, cable in enumerate(cables):
-#             for other in cables[i+1:]:
-#                 if cable.geom == other.geom and cable.CA_EQ_A == other.CA_EQ_A and cable.CA_EQ_B == other.CA_EQ_B:
-#                     self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('{}')".format(cable), filename=Cable.filename, field="geom"))
-#         
-#         ls_noeuds = list(noeuds.values())
-#         for i, noeud in enumerate(ls_noeuds):
-#             for other in ls_noeuds[i+1:]:
-#                 if noeud.geom == other.geom:
-#                     self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('{}')".format(noeud), filename=Noeud.filename, field="geom"))
-#         del ls_noeuds
-#         
-#         ls_zapbos = list(zapbos.values())
-#         for i, zapbo in enumerate(ls_zapbos):
-#             for other in ls_zapbos[i+1:]:
-#                 if zapbo.geom == other.geom:
-#                     self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('{}')".format(zapbo), filename=Zapbo.filename, field="geom"))
-#         del ls_zapbos
-#            
-#         # Arteres: comparer la géométrie à celle des noeuds
-#         for artere in arteres:
-#             if not artere.noeud_a or not artere.noeud_b:
-#                 continue
-#             
-#             buffer_a, buffer_b = artere.points[0].Buffer(TOLERANCE), artere.points[-1].Buffer(TOLERANCE)
-#             
-#             if not (buffer_a.Contains(artere.noeud_a.points[0]) and buffer_b.Contains(artere.noeud_b.points[0])) \
-#                and not (buffer_a.Contains(artere.noeud_b.points[0]) and buffer_b.Contains(artere.noeud_a.points[0])):
-# 
-#                 self.log_error(MissingItem("Pas de noeud aux coordonnées attendues ('{}')".format(artere), filename=Artere.filename, field="geom"))
-#         
-#         
-#         # Cables: comparer la géométrie à celle des equipements (on utilise en fait la geom du noeud correspondant à l'équipement)
-#         for cable in cables:
-#             if not cable.equipement_a or not cable.equipement_b or not cable.valid or not cable.equipement_a.noeud or not cable.equipement_b.noeud:
-#                 continue
-#             
-#             buffer_a, buffer_b = cable.points[0].Buffer(TOLERANCE), cable.points[-1].Buffer(TOLERANCE)
-#             
-#             if not (buffer_a.Contains(cable.equipement_a.noeud.points[0]) and buffer_b.Contains(cable.equipement_b.noeud.points[0])) \
-#                and not (buffer_a.Contains(cable.equipement_b.noeud.points[0]) and buffer_b.Contains(cable.equipement_a.noeud.points[0])):
-#             
-#                 self.log_error(MissingItem("Pas d'equipement aux coordonnées attendues ('{}')".format(cable), filename=Cable.filename, field="geom"))
-#             
-#         del buffer_a, buffer_b 
-#         
-#         # Verifie que chaque tranchée a au moins une artère
-#         arteres_emprise = Feature.buffered_union(arteres, TOLERANCE)
-#         
-#         for tranchee in tranchees:
-#             if not arteres_emprise.Contains(tranchee.geom):
-#                 self.log_error(MissingItem("Tranchée sans artère ('{}')".format(tranchee), filename=Tranchee.filename, field="-"))
-#         
-#         
-#         # Verifie que chaque cable a au moins une artère (sauf si commentaire contient 'baguette')
-#         for cable in cables:
-#             if "baguette" in cable.CA_COMMENT.lower() or not cable.valid:
-#                 continue
-#             if not arteres_emprise.Contains(cable.geom):
-#                 self.log_error(MissingItem("Cable sans artère ('{}')".format(cable), filename=Cable.filename, field="-"))
-#         
-#         del arteres_emprise
-#         
-#         # Verifie que chaque artère a au moins un cable (sauf si commentaire contient un de ces mots 'racco client adductio attente bus 'sans cable'')
-#         cables_emprise = Feature.buffered_union(cables, TOLERANCE)
-#         
-#         for artere in arteres:
-#             if any(x in artere.AR_COMMENT.lower() for x in ['racco','client','adductio','attente','bus','sans cable']):
-#                 continue
-#             if not cables_emprise.Contains(artere.geom):
-#                 self.log_error(MissingItem("Artère sans cable ('{}')".format(artere), filename=Artere.filename, field="-"))
-#                 
-#         del cables_emprise
-#                 
-#         # Contrôle des dimensions logiques
-#         for artere in arteres:
-#             try:
-#                 if not int(artere.AR_FOU_DIS) <= int(artere.AR_NB_FOUR):
-#                     self.log_error(DimensionError("Le nombre de fourreaux disponibles doit être inférieur au nombre total ('{}')".format(artere), filename=Artere.filename, field="AR_FOU_DIS"))
-#             except (TypeError, ValueError):
-#                 pass
-#         
-#         for cable in cables:
-#             try:
-#                 if not int(cable.CA_NB_FO_U) <= int(cable.CA_NB_FO):
-#                     self.log_error(DimensionError("Le nombre de fourreaux utilisés doit être inférieur au nombre total ('{}')".format(cable), filename=Cable.filename, field="CA_NB_FO_U"))
-#                 if not int(cable.CA_NB_FO_D) <= int(cable.CA_NB_FO):
-#                     self.log_error(DimensionError("Le nombre de fourreaux disponibles doit être inférieur au nombre total ('{}')".format(cable), filename=Cable.filename, field="CA_NB_FO_D"))
-#             except (TypeError, ValueError):
-#                 pass
-#         
-#         ant_db = mn.ANTDb_0()
-#         ant_db.execute("alter session set NLS_NUMERIC_CHARACTERS = '.,';") # definit le separateur decimal sur '.'
-#         
-#         # Toutes les zapbo contiennent au moins une prise
-#         for zapbo in zapbos.values():
-#             
-#             if len(zapbo.points) >= 499:
-#                 # passe l'erreur provoquée par la limite au nombre d'arguments en SQL
-#                 zapbo.nb_prises = None
-#                 continue
-#             
-#             sql = """Select SUM(NB_PRISE) AS NB_PRISES FROM SIG_ANT.FTTH_MN_PRISE_LOT z 
-#                        WHERE SDO_INSIDE(z.GEOMETRY,
-#                                       SDO_GEOMETRY(2003, 3949, SDO_POINT_TYPE(null,null,null), SDO_ELEM_INFO_ARRAY(1,1003,1),  SDO_ORDINATE_ARRAY({}))
-#                                          )='TRUE';""".format(", ".join(["{},{}".format(p.GetX(), p.GetY()) for p in zapbo.points]))
-#             
-#             zapbo.nb_prises = int(ant_db.first(sql).NB_PRISES)
-#             if not zapbo.nb_prises:
-#                 self.log_error(MissingItem("La Zapbo ne contient aucune prise: {}".format(zapbo), filename=Zapbo.filename, field="-"))
-#         
-#         # Toutes les prises de la ou les ZAPM impactées sont dans une zapbo
-#         zapms = {}
-#         # > on déduit la liste des zapms à partir de la position des zapbos
-#         for zapbo in zapbos.values():
-#             centre = zapbo.geom.Centroid()
-#             zapm = ant_db.first("""SELECT z.ID_ZAPM 
-#                                   FROM SIG_ANT.FTTH_MN_ZAPM z
-#                                   WHERE sdo_contains(z.GEOMETRY, 
-#                                                      SDO_GEOMETRY(2001, 3949, SDO_POINT_TYPE({}, {}, NULL), NULL, NULL)) = 'TRUE'
-#                                """.format(centre.GetX(), centre.GetY()))
-#             try:
-#                 zapms[zapm.ID_ZAPM].append(zapbo)
-#             except KeyError:
-#                 zapms[zapm.ID_ZAPM] = [zapbo]
-#         
-#         for id_zapm in zapms:
-#             zapm_couverture = Feature.union(zapms[id_zapm])
-#             for prise in ant_db.read("""SELECT t.X AS x, t.Y AS y
-#                                         FROM SIG_ANT.FTTH_MN_PRISE_LOT z, 
-#                                         TABLE(SDO_UTIL.GETVERTICES(z.GEOMETRY)) t 
-#                                         WHERE T_ETAT<>'OBSOLETE' AND ID_ZAPM_PARTIELLE='{}';""".format(id_zapm)):
-#                 point = ogr.Geometry(ogr.wkbPoint)
-#                 point.AddPoint(prise.x, prise.y)
-#                 if not zapm_couverture.Contains(point):
-#                     self.log_error(MissingItem("Certaines prises de la ZAPM ne sont pas comprises dans une ZAPBO: {}".format(id_zapm), filename=Zapbo.filename, field="-"))
-#         
-#         # Verifier que chaque equipement de type PBO est contenu dans une zapbo, et que le nom de la zapbo contient le nom de l'equipement
-#         
-#         for equipement in equipements.values():
-#             if not equipement.EQ_TYPE == "PBO":
-#                 continue
-#             
-#             #zapbos englobant l'equipement
-#             candidates = []
-#             for zapbo in zapbos.values():
-#                 if zapbo.geom.Contains(equipement.geom):
-#                     candidates.append(zapbo)
-#                     
-#             # le pbo doit être contenu dans une zapbo
-#             if not candidates:
-#                 self.log_error(MissingItem("Le PBO n'est contenu dans aucune ZAPBO: {}".format(equipement), filename=Equipement.filename, field="geom"))
-#                 continue
-#             
-#             # On se base sur le nom pour trouver la zapbo correspondante
-#             try:
-#                 equipement.zapbo = next((z for z in candidates if equipement.EQ_NOM in z.ID_ZAPBO))
-#             except StopIteration:
-#                 self.log_error(MissingItem("Le nom du PBO ne coincide avec le nom d'aucune des ZAPBO qui le contient: {}".format(equipement), filename=Equipement.filename, field="EQ_NOM"))
-#                 break
-#             
-#             # Controle du dimensionnement des PBO
-#             if equipement.zapbo.nb_prises is not None:
-#                 if equipement.EQ_TYPE_PH == 'PBO 6' and not equipement.zapbo.nb_prises < 6:
-#                     self.log_error(DimensionError("Le PBO 6 contient plus de 5 prises: {}".format(equipement), filename=Equipement.filename, field="-"))
-#             
-#                 if equipement.EQ_TYPE_PH == 'PBO 12' and not equipement.zapbo.nb_prises >= 6 and equipement.zapbo.nb_prises <= 8:
-#                     self.log_error(DimensionError("Le PBO 12 contient mois de 6 prises ou plus de 8 prises: {}".format(equipement), filename=Equipement.filename, field="-"))
-#         
-#             if equipement.zapbo.STATUT == "REC" and not equipement.EQ_STATUT == "REC":
-#                 self.log_error(TechnicalValidationError("Le statut du PBO n'est pas cohérent avec le statut de sa ZAPBO: {}".format(equipement), filename=Equipement.filename, field="-"))
-#         
-#             if equipement.EQ_STATUT == "REC" and not equipement.zapbo.STATUT == "REC" and not equipement.zapbo.ID_ZAPBO[:4].lower() == "att_":
-#                 self.log_error(TechnicalValidationError("Le statut du PBO n'est pas cohérent avec le statut de sa ZAPBO: {}".format(equipement), filename=Equipement.filename, field="-"))
-#         
-#         # Contrôler dans la base si des éléments portant ces codes existent à des emplacements différents
-#         for noeud in noeuds.values():
-#             sql = """SELECT z.NO_NOM, SDO_GEOM.SDO_DISTANCE(z.GEOMETRY, SDO_GEOMETRY(2001, 3949, SDO_POINT_TYPE({}, {}, NULL), NULL, NULL),0.005) AS DIST 
-#                      FROM SIG_ANT.FTTH_MN_GR_NOEUD_GEO z 
-#                      WHERE z.NO_NOM='{}';""".format(noeud.geom.GetX(), noeud.geom.GetY(), noeud.NO_NOM)
-#             existing = ant_db.first(sql)
-#             if existing:
-#                 if existing.DIST > TOLERANCE and existing.DIST < 20:
-#                     self.log_error(PositionError("La position du noeud ne correspond pas à l'existant: {}".format(noeud), filename=Noeud.filename, field="geom"))
-#                 elif existing.DIST > 20:
-#                     self.log_error(DuplicatedGeom("Un noeud portant ce nom existe déjà ailleurs sur le territoire: {}".format(noeud), filename=Noeud.filename, field="NO_NOM"))
-#         
-#         for zapbo in zapbos.values():
-#             sql = """SELECT z.ID_ZAPBO, SDO_GEOM.SDO_DISTANCE(SDO_GEOM.SDO_CENTROID(z.GEOMETRY,0.005), SDO_GEOMETRY(2001, 3949, SDO_POINT_TYPE({}, {}, NULL), NULL, NULL),0.005) AS DIST 
-#                      FROM SIG_ANT.FTTH_MN_GR_ZAPBO_GEO z 
-#                      WHERE z.ID_ZAPBO='{}';""".format(zapbo.geom.Centroid().GetX(), zapbo.geom.Centroid().GetY(), zapbo.ID_ZAPBO)
-#             existing = ant_db.first(sql)
-#             if existing:
-#                 if existing.DIST > TOLERANCE and existing.DIST < 20:
-#                     self.log_error(PositionError("La position de la ZAPBO ne correspond pas à l'existant: {}".format(zapbo), filename=Zapbo.filename, field="geom"))
-#                 elif existing.DIST > 20:
-#                     self.log_error(DuplicatedGeom("Une ZAPBO portant ce nom existe déjà ailleurs sur le territoire: {}".format(zapbo), filename=Zapbo.filename, field="ID_ZAPBO"))
-# 
-#         # Contrôle si un equipement portant ce nom existe, mais associé à un noeud différent
-#         for equipement in equipements.values():
-#             sql = """SELECT z.EQ_NOM, z.EQ_NOM_NOEUD
-#                      FROM SIG_ANT.FTTH_MN_GR_EQ_PASSIF z 
-#                      WHERE z.EQ_NOM='{}';""".format(equipement.EQ_NOM)
-#             existing = ant_db.first(sql)
-#             if existing and existing.EQ_NOM_NOEUD != equipement.EQ_NOM_NOE:
-#                 self.log_error(DuplicatedGeom("Un équipement portant ce nom ({}) existe déjà et est associé à un noeud différent ({})".format(equipement.NO_NOM, existing.EQ_NOM_NOEUD), filename=Noeud.filename, field="geom"))
-#         
-#         
-    

+ 91 - 0
core/validation_errors.py

@@ -0,0 +1,91 @@
+'''
+
+@author: olivier.massot, 2018
+'''
+
+VALIDATION_ERROR_LEVELS = {10: "MINEURE", 20: "AVERTISSEMENT", 30: "ERREUR", 40: "CRITIQUE"}
+MINOR = 10
+WARNING = 20
+ERROR = 30
+CRITICAL = 40
+
+class BaseValidationError():
+    order_ = 0
+    name = "Erreur"
+    level = ERROR
+    help = ""
+    def __init__(self, message, filename="", field=""):
+        self.message = message
+        self.filename = filename
+        self.field = field
+        
+    def __repr__(self):
+        return " - ".join(filter(None, [self.name, self.filename, self.field, self.message]))
+
+# Erreurs dans le chargement des couches
+class InputError(BaseValidationError):
+    order_ = 0
+    level = CRITICAL
+    name = "Erreur de chargement"
+
+class MissingLayer(InputError):
+    order_ = 1
+    name = "Couche Manquante"
+    
+class WrongSrid(InputError):
+    order_ = 3
+    name = "Mauvais SRID"
+
+### Erreurs dans la structure des données
+class StructureError(BaseValidationError):
+    order_ = 10
+    name = "Erreur de structure"
+    level = ERROR
+    
+class GeomTypeError(StructureError):
+    order_ = 12
+    name = "Type de géométrie invalide"
+    level = CRITICAL
+    
+class BoundingBoxError(StructureError):
+    order_ = 11
+    name = "Coordonnées hors de la zone autorisée"
+    
+class InvalidGeometry(StructureError):
+    order_ = 13
+    name = "Géométrie invalide"
+    
+class DataError(StructureError):
+    order_ = 14
+    name = "Erreur de format"
+
+# Erreurs dans le contenu, erreurs métiers
+class TechnicalValidationError(BaseValidationError):
+    order_ = 20
+    level = ERROR
+    name = "Erreur technique"
+
+class UniqueError(TechnicalValidationError):
+    order_ = 21
+    name = "Doublons dans le champs"
+
+class RelationError(TechnicalValidationError):
+    order_ = 22
+    level = CRITICAL
+    name = "Un objet lié n'existe pas"
+
+class DuplicatedGeom(TechnicalValidationError):
+    order_ = 23
+    name = "Doublon graphique"
+
+class MissingItem(TechnicalValidationError):
+    order_ = 24
+    name = "Elément manquant"
+
+class DimensionError(TechnicalValidationError):
+    order_ = 25
+    name = "Elément de dimension"
+
+class PositionError(TechnicalValidationError):
+    order_ = 26
+    name = "Erreur de positionnement"

+ 173 - 0
core/validator.py

@@ -0,0 +1,173 @@
+'''
+
+
+    @author: olivier.massot, sept. 2018
+'''
+from qgis.core import QgsProject #@UnresolvedImport
+
+from core import constants
+from core.cerberus_extend import CerberusErrorHandler, \
+    _translate_messages, ExtendedValidator
+from core.validation_errors import CRITICAL, DataError, GeomTypeError, BoundingBoxError, \
+    MissingLayer, WrongSrid
+
+class ValidatorInterruption(BaseException): 
+    pass
+
+class Checkpoint():
+    def __init__(self, name, valid=True):
+        self.name = name
+        self.valid = valid
+
+
+###########    MODELES    ################
+
+class BaseModel():
+    filename = ""
+    pk = ""
+    schema = {}
+    def __init__(self, **kwargs):
+        self.__dict__.update(kwargs)
+        
+class QgsModel():
+    layername = ""
+    pk = ""
+    geom_type = 0
+    bounding_box = (0,0,1,1)
+    schema = {}
+    
+    def __init__(self, qgs_feature):
+        self.__dict__.update(qgs_feature.__dict__)
+
+###########    VALIDATION    ################
+
+class BaseValidator():
+    schema_name = ""
+    models = {}
+    dataset = {}
+    
+    def __init__(self):
+        self.valid = True
+        self.checkpoints = []
+        self.errors = []
+        self._current_checkpoint_valid = True
+        self.dt = 0
+    
+    def checkpoint(self, title):
+        self.checkpoints.append(Checkpoint(title, self._current_checkpoint_valid))
+        self._current_checkpoint_valid = True
+        if self.errors:
+            self.valid = False
+            if self.critical_happened():
+                raise ValidatorInterruption()
+             
+    def critical_happened(self):
+        return any([err.level == CRITICAL for err in self.errors])
+             
+    def log_error(self, validation_error):
+        self._current_checkpoint_valid = False
+        self.errors.append(validation_error)
+         
+    @classmethod
+    def submit(cls, report_name=""):
+        validator = cls()
+        try:
+            validator.validate()
+        except ValidatorInterruption:
+            pass
+        
+        report = validator.build_report(validator.schema_name, report_name or "(nom inconnu)")
+        return report
+    
+    def validate(self):
+        
+        # Chargement des données en mémoire
+        self._load_layers()
+        self.checkpoint("Chargement des données")
+        
+        # Controle la structure des données (champs, formats et types)
+        self._structure_validation()
+        self.checkpoint("Contrôle de la structure des données")
+        
+        # Validation technique
+        try:
+            self._technical_validation()
+            self.checkpoint("Validation Métier")
+        except ValidatorInterruption:
+            raise
+        except:
+            self.checkpoint("Validation Métier [interrompu]")
+    
+    def _load_layers(self):
+    
+        for model in self.models:
+            layername = model.layername
+            
+            layer = next((l for l in QgsProject.instance().mapLayers().values() if l.name().lower() == layername))
+            if not layer:
+                self.log_error(MissingLayer("Couche manquante: '{}'".format(layername)))
+                continue
+            
+            self.dataset[model] = []
+            
+            if layer.crs().authid() != model.crs:
+                self.log_error(WrongSrid("Mauvaise projection: {} (attendu: {})".format(layer.crs().authid(), model.crs)))
+            
+            for feature in layer.getFeatures():
+                
+                item = model(feature)
+                
+                self.dataset[model].append(item)
+                        
+    
+    def _structure_validation(self):
+        
+        for model in self.models:
+            v = ExtendedValidator(model.schema, purge_unknown=True, error_handler=CerberusErrorHandler, require_all=True)
+            xmin, ymin, xmax, ymax = model.bounding_box
+            
+            for item in self.dataset[model]:
+
+                # geom type
+                if item.geom_type != model.geom_type:
+                    self.log_error(GeomTypeError("Type de géométrie invalide: {} (attendu: {})".format(item.geom_name, constants.GEOM_NAMES[model.geom_type]), filename=model.filename, field="geom"))
+
+                # bounding box
+                x1, y1, x2, y2 = item.bounding_box
+                if any(x < xmin or x > xmax for x in (x1, x2)) or \
+                   any(y < ymin or y > ymax for y in (y1, y2)):
+                    self.log_error(BoundingBoxError("Situé hors de l'emprise autorisée", filename=model.filename, field="geom"))
+
+                v.validate(item.__dict__)
+               
+                for field, verrors in v.errors.items():
+                    for err in verrors:
+                        self.log_error(DataError(_translate_messages(err), filename=model.filename, field=field))
+    
+    @classmethod
+    def _technical_validation(cls):
+        raise NotImplementedError()
+    
+    def build_report(self, schema, filename):
+        report = {}
+        report["schema"] = schema
+        report["filename"] = filename
+        report["checkpoints"] = [{"name": chk.name, "valid": chk.valid} for chk in self.checkpoints]
+        
+        report["errors"] = {}
+        
+        for err in self.errors:
+            if not err.name in report["errors"]:
+                report["errors"][err.name] = {"help": err.help, "order_": err.order_, "list": []}
+            
+            err_report = {"filename": err.filename or "-",
+                          "field": err.field or "-",
+                          "message": err.message}
+            if err_report not in report["errors"][err.name]["list"]:
+                report["errors"][err.name]["list"].append(err_report)
+            
+        return report
+    
+
+
+    

+ 8 - 9
main.py

@@ -39,8 +39,6 @@ logconf.start("mncheck", logging.DEBUG)
 class MnCheck:
     def __init__(self, iface):
         self.iface = iface
-        self.plugin_dir = MAIN
-        self.dlg = DlgMain()
 
         self.actions = []
         self.menu = '&MnCheck'
@@ -72,8 +70,11 @@ class MnCheck:
 
     def initGui(self):
         """Create the menu entries and toolbar icons inside the QGIS GUI."""
-        icon_path = MAIN / 'icon.png'
-        self.add_action(icon_path, text='MnCheck', callback=self.run, parent=self.iface.mainWindow())
+        
+        self.add_action(MAIN / 'icon.png', 
+                        text='MnCheck', 
+                        callback=self.run, 
+                        parent=self.iface.mainWindow())
 
     def unload(self):
         """Removes the plugin menu item and icon from QGIS GUI."""
@@ -84,8 +85,6 @@ class MnCheck:
 
     def run(self):
         """Run method that performs all the real work"""
-        self.dlg.show()
-        result = self.dlg.exec_()
-
-        if result:
-            pass
+        dlg = DlgMain(self.iface)
+        dlg.show()
+        dlg.exec_()

+ 1 - 1
schemas/common.py → schemas/_common.py

@@ -4,6 +4,6 @@
 '''
 
 XMIN, XMAX, YMIN, YMAX = 1341999, 1429750, 8147750, 8294000
-SRID = "3949"
+CRS = 'EPSG:3949' # Coordinate Reference System
 
 TOLERANCE = 1

+ 415 - 0
schemas/mn1_rec.py

@@ -0,0 +1,415 @@
+'''
+
+@author: olivier.massot, 2018
+'''
+from osgeo.ogr import Feature
+
+from core import constants
+from core.cerberus_extend import is_float, is_multi_int, is_int, \
+    is_modern_french_date
+from core.validator import QgsModel, BaseValidator
+from core.validation_errors import UniqueError, InvalidGeometry, RelationError, \
+    DuplicatedGeom, MissingItem, DimensionError, TechnicalValidationError
+from schemas._common import XMIN, YMIN, XMAX, YMAX, TOLERANCE, CRS
+
+
+class Artere(QgsModel):
+    layername = "artere_geo"
+    geom_type = constants.GEOM_LINE
+    crs = CRS
+    bounding_box = (XMIN,YMIN,XMAX,YMAX)
+    schema = {'AR_ID_INSE': {'type': 'string', 'empty': False, 'regex': r'50\d{3}'}, 
+              'AR_LONG': {'empty': False, 'validator': is_float},
+              'AR_ETAT': {'type': 'string', 'empty': False, 'allowed': ['0', '1', '2', '3', '4']}, 
+              'AR_OCCP': {'type': 'string', 'empty': False, 'allowed': ['0', '1.1', '1.2', '2', '3', '4']}, 
+              'AR_NOEUD_A': {'type': 'string', 'empty': False, 'maxlength': 20}, 
+              'AR_NOEUD_B': {'type': 'string', 'empty': False, 'maxlength': 20}, 
+              'AR_NB_FOUR': {'empty': False, 'validator': is_multi_int}, 
+              'AR_FOU_DIS': {'empty': False, 'validator': is_int}, 
+              'AR_TYPE_FO': {'type': 'string', 'multiallowed': ['PVC', 'PEHD', 'SOUS-TUBAGE PEHD', 'SOUS-TUBAGE  SOUPLE', 'FACADE', 'AERIEN', 'ENCORBELLEMENT', 'AUTRE']}, 
+              'AR_DIAM_FO': {'type': 'string', 'multiallowed': ['10', '14', '18', '25', '28', '32', '40', '45', '60', '80', '150', 'NUL']}, 
+              'AR_PRO_FOU': {'type': 'string', 'multiallowed': ['MANCHE NUMERIQUE', 'COLLECTIVITE', 'ORANGE', 'PRIVE', 'ERDF', 'AUTRE (à préciser)']}, 
+              'AR_PRO_CAB': {'type': 'string', 'empty': False, 'allowed': ['MANCHE NUMERIQUE']}, 
+              'AR_GEST_FO': {'type': 'string', 'multiallowed': ['MANCHE NUMERIQUE', 'MANCHE TELECOM', 'COLLECTIVITE', 'ORANGE', 'MANCHE FIBRE', 'PRIVE', 'ERDF', 'AUTRE (à préciser)', 'NUL']}, 
+              'AR_UTIL_FO': {'type': 'string', 'multiallowed': ['MANCHE NUMERIQUE', 'MANCHE TELECOM', 'COLLECTIVITE', 'ORANGE', 'MANCHE FIBRE', 'PRIVE', 'AUTRE (à préciser)', 'NUL']}, 
+              'AR_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
+              'AR_DATE_RE': {'empty': False, 'validator': is_modern_french_date}, 
+              'AR_REF_PLA': {'type': 'string', 'maxlength': 100}, 
+              'AR_SRC_GEO': {'type': 'string', 'maxlength': 50}, 
+              'AR_QLT_GEO': {'type': 'string', 'empty': False, 'allowed': ['A', 'B', 'C']}, 
+              'AR_PRO_MD': {'type': 'string', 'empty': False, 'default': 'MANCHE NUMERIQUE', 'allowed': ['MANCHE NUMERIQUE']}, 
+              'AR_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
+              'AR_STATUT': {'type': 'string', 'empty': False, 'allowed': ['APS', 'APD', 'EXE', 'REC']}}
+
+    def __repr__(self):
+        return "Artere {}-{}".format(self.AR_NOEUD_A, self.AR_NOEUD_B)
+
+class Cable(QgsModel):
+    layername = "cable_geo"
+    geom_type = constants.GEOM_LINE
+    crs = CRS
+    bounding_box = (XMIN,YMIN,XMAX,YMAX)
+    schema = {'CA_NUMERO': {'type': 'string', 'maxlength': 17}, 
+              'CA_TYPE': {'type': 'string', 'maxlength': 10, 'empty': False, 'allowed': ['AERIEN', 'IMMEUBLE', 'FACADE', 'MIXTE', 'SOUTERRAIN']}, 
+              'CA_ETAT': {'type': 'string', 'maxlength': 1, 'empty': False, 'allowed': ['0', '1', '2', '3', '4']}, 
+              'CA_LONG': {'validator': is_float}, 
+              'CA_EQ_A': {'type': 'string', 'maxlength': 18}, 
+              'CA_EQ_B': {'type': 'string', 'maxlength': 18}, 
+              'CA_DIAMETR': {'empty': False, 'validator': is_float}, 
+              'CA_COULEUR': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['NOIR', 'BLEU', 'BLANC']}, 
+              'CA_TECHNOL': {'type': 'string', 'maxlength': 17, 'empty': False, 'allowed': ['G657A2_M6', 'G657A2_M12']}, 
+              'CA_NB_FO': {'validator': is_int}, 
+              'CA_NB_FO_U': {'empty': False, 'validator': is_int}, 
+              'CA_NB_FO_D': {'empty': False, 'validator': is_int}, 
+              'CA_PRO': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE']}, 
+              'CA_GEST': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE FIBRE']}, 
+              'CA_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
+              'CA_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
+              'CA_STATUT': {'type': 'string', 'maxlength': 14, 'empty': False, 'allowed': ['APS', 'APD', 'EXE', 'REC']}}
+
+    def __repr__(self):
+        return "Cable {}-{}".format(self.CA_EQ_A, self.CA_EQ_B)
+    
+class Equipement(QgsModel):
+    layername = "equipement_passif"
+    geom_type = constants.GEOM_POINT
+    crs = CRS
+    bounding_box = (XMIN,YMIN,XMAX,YMAX)
+    schema = {'EQ_NOM': {'type': 'string', 'maxlength': 10, 'contains_any_of': ['PBO', 'BPE', 'BAI']}, 
+              'EQ_NOM_NOE': {'type': 'string', 'maxlength': 30}, 
+              'EQ_ETAT': {'type': 'string', 'maxlength': 1, 'empty': False, 'allowed': ['0', '1', '2', '3', '4']}, 
+              'EQ_OCCP': {'type': 'string', 'maxlength': 3, 'empty': False, 'allowed': ['0', '1.1', '1.2', '2', '3', '4']}, 
+              'EQ_TYPE': {'type': 'string', 'empty': False, 'allowed': ['PBO', 'PBOE', 'BPE', 'BAI']}, 
+              'EQ_TYPE_LQ': {'type': 'string', 'maxlength': 6, 'empty': False, 'allowed': ['PBO', 'BPE JB', 'BPE JD', 'BAIDC', 'BAIOP']}, 
+              'EQ_TYPE_PH': {'type': 'string', 'maxlength': 24, 'empty': False, 'allowed': ['PBO 6', 'PBO 12', 'BPE 12EP', 'BPE 24EP', 'BPE 48EP', 'BPE 72EP', 'BPE 96EP', 'BPE 144EP', 'BPE 288EP', 'BPE 576EP', 'BPE 720EP', 'BAI']}, 
+              'EQ_PRO': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE', 'COLLECTIVITE', 'ORANGE', 'PRIVE', 'AUTRE (à préciser)', 'NUL']}, 
+              'EQ_GEST': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE', 'MANCHE TELECOM', 'COLLECTIVITE', 'ORANGE', 'MANCHE FIBRE', 'PRIVE', 'AUTRE (à préciser)', 'NUL']}, 
+              'EQ_HAUT': {'empty': False, 'validator': is_float}, 
+              'EQ_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
+              'EQ_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
+              'EQ_STATUT': {'type': 'string', 'maxlength': 14, 'empty': False, 'allowed': ['APS', 'APD', 'EXE', 'REC']}}
+        
+    def __repr__(self):
+        return "Equipement {}".format(self.EQ_NOM)
+
+class Noeud(QgsModel):
+    layername = "noeud_geo"
+    geom_type = constants.GEOM_POINT
+    crs = CRS
+    bounding_box = (XMIN,YMIN,XMAX,YMAX)
+    schema = {'NO_NOM': {'type': 'string', 'maxlength': 30}, 
+              'NO_ID_INSE': {'type': 'string', 'empty': False, 'regex': r'50\d{3}'}, 
+              'NO_VOIE': {'type': 'string', 'maxlength': 100}, 
+              'NO_ETAT': {'type': 'string', 'maxlength': 1, 'empty': False, 'allowed': ['0', '1', '2', '3', '4']}, 
+              'NO_OCCP': {'type': 'string', 'maxlength': 3, 'empty': False, 'allowed': ['0', '1.1', '1.2', '2', '3', '4']}, 
+              'NO_TYPE': {'type': 'string', 'maxlength': 3, 'empty': False, 'allowed': ['CHA', 'POT', 'LTE', 'SEM', 'FAC', 'OUV', 'IMM']}, 
+              'NO_TYPE_LQ': {'type': 'string', 'maxlength': 10, 'empty': False, 'allowed': ['CHTIR', 'CHRACC', 'POT', 'NRO', 'PM', 'MIMO', 'FAC', 'OUV', 'IMM']}, 
+              'NO_TYPE_PH': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['CHAMBRE', 'POTEAU', 'ARMOIRE', 'SHELTER', 'BATIMENT', 'SITE MIMO', 'FACADE', 'OUVRAGE', 'IMMEUBLE']}, 
+              'NO_CODE_PH': {'type': 'string', 'maxlength': 20}, 
+              'NO_TECH_PS': {'type': 'string', 'maxlength': 20, 'multiallowed': ['COAX', 'CUT', 'ECL', 'ELEC', 'VP', 'OPT', 'NC']}, 
+              'NO_AMO': {'type': 'string', 'maxlength': 20}, 
+              'NO_PLINOX': {'required':False, 'type': 'string', 'maxlength': 3, 'allowed': ['OUI', 'NON']}, 
+              'NO_X': {'empty': False, 'validator': is_float}, 
+              'NO_Y': {'empty': False, 'validator': is_float}, 
+              'NO_PRO': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE', 'COLLECTIVITE', 'ORANGE', 'ERDF', 'PRIVE', 'ENEDIS', 'AUTRE (à préciser)', 'NUL']}, 
+              'NO_GEST': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE', 'MANCHE TELECOM', 'COLLECTIVITE', 'ORANGE', 'ERDF', 'ENEDIS', 'MANCHE FIBRE', 'PRIVE', 'AUTRE (à préciser)', 'NUL']}, 
+              'NO_HAUT': {'empty': False, 'validator': is_float}, 
+              'NO_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
+              'NO_REF_PLA': {'type': 'string', 'maxlength': 100}, 
+              'NO_SRC_GEO': {'type': 'string', 'maxlength': 50}, 
+              'NO_QLT_GEO': {'type': 'string', 'maxlength': 1, 'empty': False, 'allowed': ['A', 'B', 'C']}, 
+              'NO_PRO_MD': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE']}, 
+              'NO_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
+              'NO_STATUT': {'type': 'string', 'maxlength': 14, 'empty': False, 'allowed': ['APS', 'APD', 'EXE', 'REC']}}
+
+    def __repr__(self):
+        return "Noeud {}".format(self.NO_NOM)
+    
+class Tranchee(QgsModel):
+    layername = "tranchee_geo"
+    geom_type = constants.GEOM_LINE
+    crs = CRS
+    bounding_box = (XMIN,YMIN,XMAX,YMAX)
+    schema = {'TR_ID_INSE': {'type': 'string', 'empty': False, 'regex': r'50\d{3}'}, 
+              'TR_VOIE': {'type': 'string', 'maxlength': 200}, 
+              'TR_TYP_IMP': {'type': 'string', 'empty': False, 'allowed': ['ACCOTEMENT STABILISE', 'ACCOTEMENT NON STABILISE', 'CHAUSSEE LOURDE', 'CHAUSSEE LEGERE', 'FOSSE', 'TROTTOIR', 'ESPACE VERT', 'ENCORBELLEMENT']}, 
+              'TR_MOD_POS': {'type': 'string', 'empty': False, 'allowed': ['TRADITIONNEL', 'MICRO TRANCHEE', 'FONCAGE 60', 'FONCAGE 90', 'FONCAGE 120', 'TRANCHEUSE', 'FORAGE URBAIN', 'FORAGE RURAL', 'ENCORBELLEMENT']}, 
+              'TR_LONG': {'empty': False, 'validator': is_float}, 
+              'TR_LARG': {'empty': False, 'validator': is_float}, 
+              'TR_REVET': {'empty':True, 'type': 'string', 'allowed': ['SABLE', 'BICOUCHE', 'ENROBE', 'BETON', 'PAVE', 'TERRAIN NATUREL']}, 
+              'TR_CHARGE': {'empty': False, 'validator': is_float}, 
+              'TR_GRILLAG': {'empty':True, 'validator': is_float}, 
+              'TR_REMBLAI': {'type': 'string'}, 
+              'TR_PLYNOX': {'type': 'string', 'empty': False, 'allowed': ['OUI', 'NON']}, 
+              'TR_PRO_VOI': {'type': 'string', 'empty': False, 'allowed': ['COMMUNE', 'COMMUNAUTE DE COMMUNES', 'DEPARTEMENT', 'ETAT', 'PRIVE']}, 
+              'TR_GEST_VO': {'type': 'string', 'empty': False, 'allowed': ['COMMUNE', 'COMMUNAUTE DE COMMUNES', 'DEPARTEMENT', 'ETAT', 'PRIVE']}, 
+              'TR_SCHEMA': {'maxlength': 100, 'type': 'string'}, 
+              'TR_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
+              'TR_SRC_GEO': {'type': 'string', 'maxlength': 50}, 
+              'TR_QLT_GEO': {'type': 'string', 'empty': False, 'allowed': ['A', 'B', 'C']}, 
+              'TR_PRO_MD': {'type': 'string', 'maxlength': 20}, 
+              'TR_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
+              'TR_STATUT': {'type': 'string', 'empty': False, 'allowed': ['APS', 'APD', 'EXE', 'REC']}}
+
+    def __repr__(self):
+        return "Tranchee {}".format(self.TR_VOIE)
+    
+class Zapbo(QgsModel):
+    layername = "zapbo_geo"
+    geom_type = constants.GEOM_POLYGON
+    crs = CRS
+    bounding_box = (XMIN,YMIN,XMAX,YMAX)
+    schema = {'ID_ZAPBO': {'type': 'string', 'maxlength': 30, 'contains_any_of': ['PBO', 'BPE']}, 
+              'COMMENTAIR': {'type': 'string', 'maxlength': 254, 'empty': True}, 
+              'STATUT': {'type': 'string', 'empty': False, 'allowed': ['APS', 'APD', 'EXE', 'REC']}}
+    
+    def __repr__(self):
+        return "Zapbo {}".format(self.ID_ZAPBO)
+    
+    
+    
+    
+    
+    
+####### Validateur
+    
+    
+class Validator(BaseValidator):
+    schema_name = "Mn1 REC"
+    models = [Artere, Cable, Equipement, Noeud, Tranchee, Zapbo]
+    
+    def _technical_validation(self):
+        
+        # construction des index
+        arteres = self.dataset[Artere]
+        cables = self.dataset[Cable]
+        tranchees = self.dataset[Tranchee]
+        
+        noeuds = {}
+        for noeud in self.dataset[Noeud]:
+            if not noeud.NO_NOM in noeuds:
+                noeuds[noeud.NO_NOM] = noeud
+            else:
+                self.log_error(UniqueError("Doublons dans le champs: {}".format(noeud), layername=Noeud.layername, field="NO_NOM"))
+        
+        equipements = {}
+        for equipement in self.dataset[Equipement]:
+            if not equipement.EQ_NOM in equipements:
+                equipements[equipement.EQ_NOM] = equipement
+            else:
+                self.log_error(UniqueError("Doublons dans le champs: {}".format(equipement), layername=Equipement.layername, field="EQ_NOM"))
+                
+        zapbos = {}
+        for zapbo in self.dataset[Zapbo]:
+            if not zapbo.ID_ZAPBO in zapbos:
+                zapbos[zapbo.ID_ZAPBO] = zapbo
+            else:
+                self.log_error(UniqueError("Doublons dans le champs: {}".format(zapbo), layername=Zapbo.layername, field="ID_ZAPBO"))
+        
+        # contrôle de la validité des géométries
+        for artere in arteres:
+            if not artere.valid:
+                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(artere), layername=Artere.layername, field="geom"))
+        for tranchee in tranchees:
+            if not tranchee.valid:
+                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(tranchee), layername=Tranchee.layername, field="geom"))
+        for cable in cables:
+            if not "baguette" in cable.CA_COMMENT.lower() and not cable.valid:
+                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(cable), layername=Cable.layername, field="geom"))
+        for noeud in noeuds.values():
+            if not noeud.valid:
+                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(noeud), layername=Noeud.layername, field="geom"))
+        for equipement in equipements.values():
+            if not equipement.valid:
+                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(equipement), layername=Equipement.layername, field="geom"))
+        for zapbo in zapbos.values():
+            if not zapbo.valid:
+                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(zapbo), layername=Zapbo.layername, field="geom"))
+        
+        # rattachement les noeuds aux artères     
+        for artere in arteres:
+            try:
+                artere.noeud_a = noeuds[artere.AR_NOEUD_A]
+            except KeyError:
+                artere.noeud_a = None
+                self.log_error(RelationError("Le noeud '{}' n'existe pas".format(artere.AR_NOEUD_A), layername=Artere.layername, field="AR_NOEUD_A"))
+                
+            try:
+                artere.noeud_b = noeuds[artere.AR_NOEUD_B]
+            except KeyError:
+                artere.noeud_b = None
+                self.log_error(RelationError("Le noeud '{}' n'existe pas".format(artere.AR_NOEUD_B), layername=Artere.layername, field="AR_NOEUD_A"))
+        
+        # rattachement des equipements aux cables
+        for cable in cables:
+            try:
+                cable.equipement_a = equipements[cable.CA_EQ_A]
+            except KeyError:
+                cable.equipement_a = None
+                self.log_error(RelationError("L'équipement '{}' n'existe pas".format(cable.CA_EQ_A), layername=Cable.layername, field="CA_EQ_A"))
+                
+            try:
+                cable.equipement_b = equipements[cable.CA_EQ_B]
+            except KeyError:
+                cable.equipement_b = None
+                self.log_error(RelationError("L'équipement '{}' n'existe pas".format(cable.CA_EQ_B), layername=Cable.layername, field="CA_EQ_B"))
+
+        # rattachement des equipements aux noeuds
+        for equipement in equipements.values():
+            try:
+                equipement.noeud = noeuds[equipement.EQ_NOM_NOE]
+            except KeyError:
+                equipement.noeud = None
+                self.log_error(RelationError("Le noeud '{}' n'existe pas".format(equipement.EQ_NOM_NOE, equipement.EQ_NOM), layername=Equipement.layername, field="EQ_NOM_NOE"))
+
+        # verifie que tous les equipements sont l'equipement B d'au moins un cable
+        equipements_b = [cable.CA_EQ_B for cable in cables]
+        for eq_id in equipements:
+            if equipements[eq_id].EQ_TYPE == "BAI":
+                continue
+            if not eq_id in equipements_b:
+                self.log_error(RelationError("L'equipement '{}' n'est l'équipement B d'aucun cable".format(eq_id), layername=Equipement.layername, field="EQ_NOM"))
+
+        # controle des doublons graphiques
+        for i, tranchee in enumerate(tranchees):
+            for other in tranchees[i+1:]:
+                if tranchee.geom == other.geom:
+                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée".format(tranchee), layername=Tranchee.layername, field="geom"))
+                    
+        for i, artere in enumerate(arteres):
+            for other in arteres[i+1:]:
+                if artere.geom == other.geom:
+                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('{}')".format(artere), layername=Artere.layername, field="geom"))
+
+        for i, cable in enumerate(cables):
+            for other in cables[i+1:]:
+                if cable.geom == other.geom and cable.CA_EQ_A == other.CA_EQ_A and cable.CA_EQ_B == other.CA_EQ_B:
+                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('{}')".format(cable), layername=Cable.layername, field="geom"))
+        
+        ls_noeuds = list(noeuds.values())
+        for i, noeud in enumerate(ls_noeuds):
+            for other in ls_noeuds[i+1:]:
+                if noeud.geom == other.geom:
+                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('{}')".format(noeud), layername=Noeud.layername, field="geom"))
+        del ls_noeuds
+        
+        ls_zapbos = list(zapbos.values())
+        for i, zapbo in enumerate(ls_zapbos):
+            for other in ls_zapbos[i+1:]:
+                if zapbo.geom == other.geom:
+                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('{}')".format(zapbo), layername=Zapbo.layername, field="geom"))
+        del ls_zapbos
+           
+        # Arteres: comparer la géométrie à celle des noeuds
+        for artere in arteres:
+            if not artere.noeud_a or not artere.noeud_b:
+                continue
+            
+            buffer_a, buffer_b = artere.points[0].Buffer(TOLERANCE), artere.points[-1].Buffer(TOLERANCE)
+            
+            if not (buffer_a.Contains(artere.noeud_a.points[0]) and buffer_b.Contains(artere.noeud_b.points[0])) \
+               and not (buffer_a.Contains(artere.noeud_b.points[0]) and buffer_b.Contains(artere.noeud_a.points[0])):
+
+                self.log_error(MissingItem("Pas de noeud aux coordonnées attendues ('{}')".format(artere), layername=Artere.layername, field="geom"))
+        
+        
+        # Cables: comparer la géométrie à celle des equipements (on utilise en fait la geom du noeud correspondant à l'équipement)
+        for cable in cables:
+            if not cable.equipement_a or not cable.equipement_b or not cable.valid or not cable.equipement_a.noeud or not cable.equipement_b.noeud:
+                continue
+            
+            buffer_a, buffer_b = cable.points[0].Buffer(TOLERANCE), cable.points[-1].Buffer(TOLERANCE)
+            
+            if not (buffer_a.Contains(cable.equipement_a.noeud.points[0]) and buffer_b.Contains(cable.equipement_b.noeud.points[0])) \
+               and not (buffer_a.Contains(cable.equipement_b.noeud.points[0]) and buffer_b.Contains(cable.equipement_a.noeud.points[0])):
+            
+                self.log_error(MissingItem("Pas d'equipement aux coordonnées attendues ('{}')".format(cable), layername=Cable.layername, field="geom"))
+            
+        del buffer_a, buffer_b 
+        
+        # Verifie que chaque tranchée a au moins une artère
+        arteres_emprise = Feature.buffered_union(arteres, TOLERANCE)
+        
+        for tranchee in tranchees:
+            if not arteres_emprise.Contains(tranchee.geom):
+                self.log_error(MissingItem("Tranchée sans artère ('{}')".format(tranchee), layername=Tranchee.layername, field="-"))
+        
+        
+        # Verifie que chaque cable a au moins une artère (sauf si commentaire contient 'baguette')
+        for cable in cables:
+            if "baguette" in cable.CA_COMMENT.lower() or not cable.valid:
+                continue
+            if not arteres_emprise.Contains(cable.geom):
+                self.log_error(MissingItem("Cable sans artère ('{}')".format(cable), layername=Cable.layername, field="-"))
+        
+        del arteres_emprise
+        
+        # Verifie que chaque artère a au moins un cable (sauf si commentaire contient un de ces mots 'racco client adductio attente bus 'sans cable'')
+        cables_emprise = Feature.buffered_union(cables, TOLERANCE)
+        
+        for artere in arteres:
+            if any(x in artere.AR_COMMENT.lower() for x in ['racco','client','adductio','attente','bus','sans cable']):
+                continue
+            if not cables_emprise.Contains(artere.geom):
+                self.log_error(MissingItem("Artère sans cable ('{}')".format(artere), layername=Artere.layername, field="-"))
+                
+        del cables_emprise
+                
+        # Contrôle des dimensions logiques
+        for artere in arteres:
+            try:
+                if not int(artere.AR_FOU_DIS) <= int(artere.AR_NB_FOUR):
+                    self.log_error(DimensionError("Le nombre de fourreaux disponibles doit être inférieur au nombre total ('{}')".format(artere), layername=Artere.layername, field="AR_FOU_DIS"))
+            except (TypeError, ValueError):
+                pass
+        
+        for cable in cables:
+            try:
+                if not int(cable.CA_NB_FO_U) <= int(cable.CA_NB_FO):
+                    self.log_error(DimensionError("Le nombre de fourreaux utilisés doit être inférieur au nombre total ('{}')".format(cable), layername=Cable.layername, field="CA_NB_FO_U"))
+                if not int(cable.CA_NB_FO_D) <= int(cable.CA_NB_FO):
+                    self.log_error(DimensionError("Le nombre de fourreaux disponibles doit être inférieur au nombre total ('{}')".format(cable), layername=Cable.layername, field="CA_NB_FO_D"))
+            except (TypeError, ValueError):
+                pass
+        
+        
+        # Verifier que chaque equipement de type PBO est contenu dans une zapbo, et que le nom de la zapbo contient le nom de l'equipement
+        
+        for equipement in equipements.values():
+            if not equipement.EQ_TYPE == "PBO":
+                continue
+            
+            #zapbos englobant l'equipement
+            candidates = []
+            for zapbo in zapbos.values():
+                if zapbo.geom.Contains(equipement.geom):
+                    candidates.append(zapbo)
+                    
+            # le pbo doit être contenu dans une zapbo
+            if not candidates:
+                self.log_error(MissingItem("Le PBO n'est contenu dans aucune ZAPBO: {}".format(equipement), layername=Equipement.layername, field="geom"))
+                continue
+            
+            # On se base sur le nom pour trouver la zapbo correspondante
+            try:
+                equipement.zapbo = next((z for z in candidates if equipement.EQ_NOM in z.ID_ZAPBO))
+            except StopIteration:
+                self.log_error(MissingItem("Le nom du PBO ne coincide avec le nom d'aucune des ZAPBO qui le contient: {}".format(equipement), layername=Equipement.layername, field="EQ_NOM"))
+                break
+            
+            if equipement.zapbo.nb_prises is None:
+                equipement.zapbo.nb_prises = 0
+            
+            # Controle du dimensionnement des PBO
+            if equipement.EQ_TYPE_PH == 'PBO 6' and not equipement.zapbo.nb_prises < 6:
+                self.log_error(DimensionError("Le PBO 6 contient plus de 5 prises: {}".format(equipement), layername=Equipement.layername, field="-"))
+        
+            if equipement.EQ_TYPE_PH == 'PBO 12' and not equipement.zapbo.nb_prises >= 6 and equipement.zapbo.nb_prises <= 8:
+                self.log_error(DimensionError("Le PBO 12 contient mois de 6 prises ou plus de 8 prises: {}".format(equipement), layername=Equipement.layername, field="-"))
+        
+            if equipement.zapbo.STATUT == "REC" and not equipement.EQ_STATUT == "REC":
+                self.log_error(TechnicalValidationError("Le statut du PBO n'est pas cohérent avec le statut de sa ZAPBO: {}".format(equipement), layername=Equipement.layername, field="-"))
+        
+            if equipement.EQ_STATUT == "REC" and not equipement.zapbo.STATUT == "REC" and not equipement.zapbo.ID_ZAPBO[:4].lower() == "att_":
+                self.log_error(TechnicalValidationError("Le statut du PBO n'est pas cohérent avec le statut de sa ZAPBO: {}".format(equipement), layername=Equipement.layername, field="-"))
+        

+ 0 - 3
schemas/mn1_rec/__init__.py

@@ -1,3 +0,0 @@
-
-from schemas.mn1_rec.validator import Mn1RecValidator
-validator = Mn1RecValidator

+ 0 - 157
schemas/mn1_rec/models.py

@@ -1,157 +0,0 @@
-'''
-
-@author: olivier.massot, 2018
-'''
-from core import gis_
-from core.cerberus_extend import is_int, is_float, \
-    is_modern_french_date, is_multi_int
-from core.validation import BaseGeoModel
-from schemas.common import XMIN, YMIN, XMAX, YMAX
-
-
-class Artere(BaseGeoModel):
-    filename = "artere_geo.shp"
-    geom_type = gis_.GEOM_LINE
-    bounding_box = (XMIN,YMIN,XMAX,YMAX)
-    schema = {'AR_ID_INSE': {'type': 'string', 'empty': False, 'regex': r'50\d{3}'}, 
-              'AR_LONG': {'empty': False, 'validator': is_float},
-              'AR_ETAT': {'type': 'string', 'empty': False, 'allowed': ['0', '1', '2', '3', '4']}, 
-              'AR_OCCP': {'type': 'string', 'empty': False, 'allowed': ['0', '1.1', '1.2', '2', '3', '4']}, 
-              'AR_NOEUD_A': {'type': 'string', 'empty': False, 'maxlength': 20}, 
-              'AR_NOEUD_B': {'type': 'string', 'empty': False, 'maxlength': 20}, 
-              'AR_NB_FOUR': {'empty': False, 'validator': is_multi_int}, 
-              'AR_FOU_DIS': {'empty': False, 'validator': is_int}, 
-              'AR_TYPE_FO': {'type': 'string', 'multiallowed': ['PVC', 'PEHD', 'SOUS-TUBAGE PEHD', 'SOUS-TUBAGE  SOUPLE', 'FACADE', 'AERIEN', 'ENCORBELLEMENT', 'AUTRE']}, 
-              'AR_DIAM_FO': {'type': 'string', 'multiallowed': ['10', '14', '18', '25', '28', '32', '40', '45', '60', '80', '150', 'NUL']}, 
-              'AR_PRO_FOU': {'type': 'string', 'multiallowed': ['MANCHE NUMERIQUE', 'COLLECTIVITE', 'ORANGE', 'PRIVE', 'ERDF', 'AUTRE (à préciser)']}, 
-              'AR_PRO_CAB': {'type': 'string', 'empty': False, 'allowed': ['MANCHE NUMERIQUE']}, 
-              'AR_GEST_FO': {'type': 'string', 'multiallowed': ['MANCHE NUMERIQUE', 'MANCHE TELECOM', 'COLLECTIVITE', 'ORANGE', 'MANCHE FIBRE', 'PRIVE', 'ERDF', 'AUTRE (à préciser)', 'NUL']}, 
-              'AR_UTIL_FO': {'type': 'string', 'multiallowed': ['MANCHE NUMERIQUE', 'MANCHE TELECOM', 'COLLECTIVITE', 'ORANGE', 'MANCHE FIBRE', 'PRIVE', 'AUTRE (à préciser)', 'NUL']}, 
-              'AR_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
-              'AR_DATE_RE': {'empty': False, 'validator': is_modern_french_date}, 
-              'AR_REF_PLA': {'type': 'string', 'maxlength': 100}, 
-              'AR_SRC_GEO': {'type': 'string', 'maxlength': 50}, 
-              'AR_QLT_GEO': {'type': 'string', 'empty': False, 'allowed': ['A', 'B', 'C']}, 
-              'AR_PRO_MD': {'type': 'string', 'empty': False, 'default': 'MANCHE NUMERIQUE', 'allowed': ['MANCHE NUMERIQUE']}, 
-              'AR_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
-              'AR_STATUT': {'type': 'string', 'empty': False, 'allowed': ['APS', 'APD', 'EXE', 'REC']}}
-
-    def __repr__(self):
-        return "Artere {}-{}".format(self.AR_NOEUD_A, self.AR_NOEUD_B)
-
-class Cable(BaseGeoModel):
-    filename = "cable_geo.shp"
-    geom_type = gis_.GEOM_LINE
-    bounding_box = (XMIN,YMIN,XMAX,YMAX)
-    schema = {'CA_NUMERO': {'type': 'string', 'maxlength': 17}, 
-              'CA_TYPE': {'type': 'string', 'maxlength': 10, 'empty': False, 'allowed': ['AERIEN', 'IMMEUBLE', 'FACADE', 'MIXTE', 'SOUTERRAIN']}, 
-              'CA_ETAT': {'type': 'string', 'maxlength': 1, 'empty': False, 'allowed': ['0', '1', '2', '3', '4']}, 
-              'CA_LONG': {'validator': is_float}, 
-              'CA_EQ_A': {'type': 'string', 'maxlength': 18}, 
-              'CA_EQ_B': {'type': 'string', 'maxlength': 18}, 
-              'CA_DIAMETR': {'empty': False, 'validator': is_float}, 
-              'CA_COULEUR': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['NOIR', 'BLEU', 'BLANC']}, 
-              'CA_TECHNOL': {'type': 'string', 'maxlength': 17, 'empty': False, 'allowed': ['G657A2_M6', 'G657A2_M12']}, 
-              'CA_NB_FO': {'validator': is_int}, 
-              'CA_NB_FO_U': {'empty': False, 'validator': is_int}, 
-              'CA_NB_FO_D': {'empty': False, 'validator': is_int}, 
-              'CA_PRO': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE']}, 
-              'CA_GEST': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE FIBRE']}, 
-              'CA_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
-              'CA_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
-              'CA_STATUT': {'type': 'string', 'maxlength': 14, 'empty': False, 'allowed': ['APS', 'APD', 'EXE', 'REC']}}
-
-    def __repr__(self):
-        return "Cable {}-{}".format(self.CA_EQ_A, self.CA_EQ_B)
-    
-class Equipement(BaseGeoModel):
-    filename = "equipement_passif.shp"
-    geom_type = gis_.GEOM_POINT
-    bounding_box = (XMIN,YMIN,XMAX,YMAX)
-    schema = {'EQ_NOM': {'type': 'string', 'maxlength': 10, 'contains_any_of': ['PBO', 'BPE', 'BAI']}, 
-              'EQ_NOM_NOE': {'type': 'string', 'maxlength': 30}, 
-              'EQ_ETAT': {'type': 'string', 'maxlength': 1, 'empty': False, 'allowed': ['0', '1', '2', '3', '4']}, 
-              'EQ_OCCP': {'type': 'string', 'maxlength': 3, 'empty': False, 'allowed': ['0', '1.1', '1.2', '2', '3', '4']}, 
-              'EQ_TYPE': {'type': 'string', 'empty': False, 'allowed': ['PBO', 'PBOE', 'BPE', 'BAI']}, 
-              'EQ_TYPE_LQ': {'type': 'string', 'maxlength': 6, 'empty': False, 'allowed': ['PBO', 'BPE JB', 'BPE JD', 'BAIDC', 'BAIOP']}, 
-              'EQ_TYPE_PH': {'type': 'string', 'maxlength': 24, 'empty': False, 'allowed': ['PBO 6', 'PBO 12', 'BPE 12EP', 'BPE 24EP', 'BPE 48EP', 'BPE 72EP', 'BPE 96EP', 'BPE 144EP', 'BPE 288EP', 'BPE 576EP', 'BPE 720EP', 'BAI']}, 
-              'EQ_PRO': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE', 'COLLECTIVITE', 'ORANGE', 'PRIVE', 'AUTRE (à préciser)', 'NUL']}, 
-              'EQ_GEST': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE', 'MANCHE TELECOM', 'COLLECTIVITE', 'ORANGE', 'MANCHE FIBRE', 'PRIVE', 'AUTRE (à préciser)', 'NUL']}, 
-              'EQ_HAUT': {'empty': False, 'validator': is_float}, 
-              'EQ_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
-              'EQ_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
-              'EQ_STATUT': {'type': 'string', 'maxlength': 14, 'empty': False, 'allowed': ['APS', 'APD', 'EXE', 'REC']}}
-        
-    def __repr__(self):
-        return "Equipement {}".format(self.EQ_NOM)
-
-class Noeud(BaseGeoModel):
-    filename = "noeud_geo.shp"
-    geom_type = gis_.GEOM_POINT
-    bounding_box = (XMIN,YMIN,XMAX,YMAX)
-    schema = {'NO_NOM': {'type': 'string', 'maxlength': 30}, 
-              'NO_ID_INSE': {'type': 'string', 'empty': False, 'regex': r'50\d{3}'}, 
-              'NO_VOIE': {'type': 'string', 'maxlength': 100}, 
-              'NO_ETAT': {'type': 'string', 'maxlength': 1, 'empty': False, 'allowed': ['0', '1', '2', '3', '4']}, 
-              'NO_OCCP': {'type': 'string', 'maxlength': 3, 'empty': False, 'allowed': ['0', '1.1', '1.2', '2', '3', '4']}, 
-              'NO_TYPE': {'type': 'string', 'maxlength': 3, 'empty': False, 'allowed': ['CHA', 'POT', 'LTE', 'SEM', 'FAC', 'OUV', 'IMM']}, 
-              'NO_TYPE_LQ': {'type': 'string', 'maxlength': 10, 'empty': False, 'allowed': ['CHTIR', 'CHRACC', 'POT', 'NRO', 'PM', 'MIMO', 'FAC', 'OUV', 'IMM']}, 
-              'NO_TYPE_PH': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['CHAMBRE', 'POTEAU', 'ARMOIRE', 'SHELTER', 'BATIMENT', 'SITE MIMO', 'FACADE', 'OUVRAGE', 'IMMEUBLE']}, 
-              'NO_CODE_PH': {'type': 'string', 'maxlength': 20}, 
-              'NO_TECH_PS': {'type': 'string', 'maxlength': 20, 'multiallowed': ['COAX', 'CUT', 'ECL', 'ELEC', 'VP', 'OPT', 'NC']}, 
-              'NO_AMO': {'type': 'string', 'maxlength': 20}, 
-              'NO_PLINOX': {'required':False, 'type': 'string', 'maxlength': 3, 'allowed': ['OUI', 'NON']}, 
-              'NO_X': {'empty': False, 'validator': is_float}, 
-              'NO_Y': {'empty': False, 'validator': is_float}, 
-              'NO_PRO': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE', 'COLLECTIVITE', 'ORANGE', 'ERDF', 'PRIVE', 'ENEDIS', 'AUTRE (à préciser)', 'NUL']}, 
-              'NO_GEST': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE', 'MANCHE TELECOM', 'COLLECTIVITE', 'ORANGE', 'ERDF', 'ENEDIS', 'MANCHE FIBRE', 'PRIVE', 'AUTRE (à préciser)', 'NUL']}, 
-              'NO_HAUT': {'empty': False, 'validator': is_float}, 
-              'NO_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
-              'NO_REF_PLA': {'type': 'string', 'maxlength': 100}, 
-              'NO_SRC_GEO': {'type': 'string', 'maxlength': 50}, 
-              'NO_QLT_GEO': {'type': 'string', 'maxlength': 1, 'empty': False, 'allowed': ['A', 'B', 'C']}, 
-              'NO_PRO_MD': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE']}, 
-              'NO_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
-              'NO_STATUT': {'type': 'string', 'maxlength': 14, 'empty': False, 'allowed': ['APS', 'APD', 'EXE', 'REC']}}
-
-    def __repr__(self):
-        return "Noeud {}".format(self.NO_NOM)
-    
-class Tranchee(BaseGeoModel):
-    filename = "tranchee_geo.shp"
-    geom_type = gis_.GEOM_LINE
-    bounding_box = (XMIN,YMIN,XMAX,YMAX)
-    schema = {'TR_ID_INSE': {'type': 'string', 'empty': False, 'regex': r'50\d{3}'}, 
-              'TR_VOIE': {'type': 'string', 'maxlength': 200}, 
-              'TR_TYP_IMP': {'type': 'string', 'empty': False, 'allowed': ['ACCOTEMENT STABILISE', 'ACCOTEMENT NON STABILISE', 'CHAUSSEE LOURDE', 'CHAUSSEE LEGERE', 'FOSSE', 'TROTTOIR', 'ESPACE VERT', 'ENCORBELLEMENT']}, 
-              'TR_MOD_POS': {'type': 'string', 'empty': False, 'allowed': ['TRADITIONNEL', 'MICRO TRANCHEE', 'FONCAGE 60', 'FONCAGE 90', 'FONCAGE 120', 'TRANCHEUSE', 'FORAGE URBAIN', 'FORAGE RURAL', 'ENCORBELLEMENT']}, 
-              'TR_LONG': {'empty': False, 'validator': is_float}, 
-              'TR_LARG': {'empty': False, 'validator': is_float}, 
-              'TR_REVET': {'empty':True, 'type': 'string', 'allowed': ['SABLE', 'BICOUCHE', 'ENROBE', 'BETON', 'PAVE', 'TERRAIN NATUREL']}, 
-              'TR_CHARGE': {'empty': False, 'validator': is_float}, 
-              'TR_GRILLAG': {'empty':True, 'validator': is_float}, 
-              'TR_REMBLAI': {'type': 'string'}, 
-              'TR_PLYNOX': {'type': 'string', 'empty': False, 'allowed': ['OUI', 'NON']}, 
-              'TR_PRO_VOI': {'type': 'string', 'empty': False, 'allowed': ['COMMUNE', 'COMMUNAUTE DE COMMUNES', 'DEPARTEMENT', 'ETAT', 'PRIVE']}, 
-              'TR_GEST_VO': {'type': 'string', 'empty': False, 'allowed': ['COMMUNE', 'COMMUNAUTE DE COMMUNES', 'DEPARTEMENT', 'ETAT', 'PRIVE']}, 
-              'TR_SCHEMA': {'maxlength': 100, 'type': 'string'}, 
-              'TR_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
-              'TR_SRC_GEO': {'type': 'string', 'maxlength': 50}, 
-              'TR_QLT_GEO': {'type': 'string', 'empty': False, 'allowed': ['A', 'B', 'C']}, 
-              'TR_PRO_MD': {'type': 'string', 'maxlength': 20}, 
-              'TR_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
-              'TR_STATUT': {'type': 'string', 'empty': False, 'allowed': ['APS', 'APD', 'EXE', 'REC']}}
-
-    def __repr__(self):
-        return "Tranchee {}".format(self.TR_VOIE)
-    
-class Zapbo(BaseGeoModel):
-    filename = "zapbo_geo.shp"
-    geom_type = gis_.GEOM_POLYGON
-    bounding_box = (XMIN,YMIN,XMAX,YMAX)
-    schema = {'ID_ZAPBO': {'type': 'string', 'maxlength': 30, 'contains_any_of': ['PBO', 'BPE']}, 
-              'COMMENTAIR': {'type': 'string', 'maxlength': 254, 'empty': True}, 
-              'STATUT': {'type': 'string', 'empty': False, 'allowed': ['APS', 'APD', 'EXE', 'REC']}}
-    
-    def __repr__(self):
-        return "Zapbo {}".format(self.ID_ZAPBO)

+ 0 - 28
schemas/mn1_rec/readme.md

@@ -1,28 +0,0 @@
-## Tests effectués
-
-* **Chargement des données**
-
-  * Présence des fichiers attendus: "artere_geo.shp", "cable_geo.shp", "equipement_passif.shp", "noeud_geo.shp", "tranchee_geo.shp","zapbo_geo.shp"
-  * Fichiers au format SHP
-  * Types de géométrie
-
-* **Structure**
-
-  * Les coordonnées des entités sont dans la zone d'emprise du département de la Manche
-  * Les champs attendus sont présents
-  * Les champs obligatoires sont renseignés
-  * Type des données (entiers, décimaux, dates...)
-  * Les champs ne dépassent pas la longueur autorisée
-  * Contrôle des valeurs autorisées
-  
-* **Validation Métier**
-
-  * Contrôle des doubloins dans les codes des noeuds, equipements, zapbo.
-  * Contrôle des cardinalités: correspondance artère/noeuds, cables/equipements, equipement/noeud...etc.
-  * Contrôle des doublons graphiques
-  * Cohérence de la géographie (ex: les noeuds sont situés aux extrémités des artères)
-  * Contrôle de la logique des données (ex: le nombre des fourreaux utilisés n'est pas supérieur au nombre total)
-  * Comparaison avec la base de données (ex: un noeud portant le même code n'existe pas déjà ailleurs sur le territoire)
-  
-
-  

+ 0 - 258
schemas/mn1_rec/validator.py

@@ -1,258 +0,0 @@
-'''
-
-@author: olivier.massot, 2018
-'''
-from core.validation import NetgeoValidator, RelationError, \
-    DuplicatedGeom, MissingItem, DimensionError, TechnicalValidationError, \
-    InvalidGeometry, UniqueError
-from schemas.common import TOLERANCE
-from schemas.mn1_rec.models import Artere, Cable, Equipement, Noeud, \
-    Tranchee, Zapbo
-
-
-class Mn1RecValidator(NetgeoValidator):
-    schema_name = "Mn1 REC"
-    models = [Artere, Cable, Equipement, Noeud, Tranchee, Zapbo]
-    
-    def _technical_validation(self):
-        
-        # construction des index
-        arteres = self.dataset[Artere]
-        cables = self.dataset[Cable]
-        tranchees = self.dataset[Tranchee]
-        
-        noeuds = {}
-        for noeud in self.dataset[Noeud]:
-            if not noeud.NO_NOM in noeuds:
-                noeuds[noeud.NO_NOM] = noeud
-            else:
-                self.log_error(UniqueError("Doublons dans le champs: {}".format(noeud), filename=Noeud.filename, field="NO_NOM"))
-        
-        equipements = {}
-        for equipement in self.dataset[Equipement]:
-            if not equipement.EQ_NOM in equipements:
-                equipements[equipement.EQ_NOM] = equipement
-            else:
-                self.log_error(UniqueError("Doublons dans le champs: {}".format(equipement), filename=Equipement.filename, field="EQ_NOM"))
-                
-        zapbos = {}
-        for zapbo in self.dataset[Zapbo]:
-            if not zapbo.ID_ZAPBO in zapbos:
-                zapbos[zapbo.ID_ZAPBO] = zapbo
-            else:
-                self.log_error(UniqueError("Doublons dans le champs: {}".format(zapbo), filename=Zapbo.filename, field="ID_ZAPBO"))
-        
-        # contrôle de la validité des géométries
-        for artere in arteres:
-            if not artere.valid:
-                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(artere), filename=Artere.filename, field="geom"))
-        for tranchee in tranchees:
-            if not tranchee.valid:
-                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(tranchee), filename=Tranchee.filename, field="geom"))
-        for cable in cables:
-            if not "baguette" in cable.CA_COMMENT.lower() and not cable.valid:
-                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(cable), filename=Cable.filename, field="geom"))
-        for noeud in noeuds.values():
-            if not noeud.valid:
-                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(noeud), filename=Noeud.filename, field="geom"))
-        for equipement in equipements.values():
-            if not equipement.valid:
-                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(equipement), filename=Equipement.filename, field="geom"))
-        for zapbo in zapbos.values():
-            if not zapbo.valid:
-                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(zapbo), filename=Zapbo.filename, field="geom"))
-        
-        # rattachement les noeuds aux artères     
-        for artere in arteres:
-            try:
-                artere.noeud_a = noeuds[artere.AR_NOEUD_A]
-            except KeyError:
-                artere.noeud_a = None
-                self.log_error(RelationError("Le noeud '{}' n'existe pas".format(artere.AR_NOEUD_A), filename=Artere.filename, field="AR_NOEUD_A"))
-                
-            try:
-                artere.noeud_b = noeuds[artere.AR_NOEUD_B]
-            except KeyError:
-                artere.noeud_b = None
-                self.log_error(RelationError("Le noeud '{}' n'existe pas".format(artere.AR_NOEUD_B), filename=Artere.filename, field="AR_NOEUD_A"))
-        
-        # rattachement des equipements aux cables
-        for cable in cables:
-            try:
-                cable.equipement_a = equipements[cable.CA_EQ_A]
-            except KeyError:
-                cable.equipement_a = None
-                self.log_error(RelationError("L'équipement '{}' n'existe pas".format(cable.CA_EQ_A), filename=Cable.filename, field="CA_EQ_A"))
-                
-            try:
-                cable.equipement_b = equipements[cable.CA_EQ_B]
-            except KeyError:
-                cable.equipement_b = None
-                self.log_error(RelationError("L'équipement '{}' n'existe pas".format(cable.CA_EQ_B), filename=Cable.filename, field="CA_EQ_B"))
-
-        # rattachement des equipements aux noeuds
-        for equipement in equipements.values():
-            try:
-                equipement.noeud = noeuds[equipement.EQ_NOM_NOE]
-            except KeyError:
-                equipement.noeud = None
-                self.log_error(RelationError("Le noeud '{}' n'existe pas".format(equipement.EQ_NOM_NOE, equipement.EQ_NOM), filename=Equipement.filename, field="EQ_NOM_NOE"))
-
-        # verifie que tous les equipements sont l'equipement B d'au moins un cable
-        equipements_b = [cable.CA_EQ_B for cable in cables]
-        for eq_id in equipements:
-            if equipements[eq_id].EQ_TYPE == "BAI":
-                continue
-            if not eq_id in equipements_b:
-                self.log_error(RelationError("L'equipement '{}' n'est l'équipement B d'aucun cable".format(eq_id), filename=Equipement.filename, field="EQ_NOM"))
-
-        # controle des doublons graphiques
-        for i, tranchee in enumerate(tranchees):
-            for other in tranchees[i+1:]:
-                if tranchee.geom == other.geom:
-                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée".format(tranchee), filename=Tranchee.filename, field="geom"))
-                    
-        for i, artere in enumerate(arteres):
-            for other in arteres[i+1:]:
-                if artere.geom == other.geom:
-                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('{}')".format(artere), filename=Artere.filename, field="geom"))
-
-        for i, cable in enumerate(cables):
-            for other in cables[i+1:]:
-                if cable.geom == other.geom and cable.CA_EQ_A == other.CA_EQ_A and cable.CA_EQ_B == other.CA_EQ_B:
-                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('{}')".format(cable), filename=Cable.filename, field="geom"))
-        
-        ls_noeuds = list(noeuds.values())
-        for i, noeud in enumerate(ls_noeuds):
-            for other in ls_noeuds[i+1:]:
-                if noeud.geom == other.geom:
-                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('{}')".format(noeud), filename=Noeud.filename, field="geom"))
-        del ls_noeuds
-        
-        ls_zapbos = list(zapbos.values())
-        for i, zapbo in enumerate(ls_zapbos):
-            for other in ls_zapbos[i+1:]:
-                if zapbo.geom == other.geom:
-                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('{}')".format(zapbo), filename=Zapbo.filename, field="geom"))
-        del ls_zapbos
-           
-        # Arteres: comparer la géométrie à celle des noeuds
-        for artere in arteres:
-            if not artere.noeud_a or not artere.noeud_b:
-                continue
-            
-            buffer_a, buffer_b = artere.points[0].Buffer(TOLERANCE), artere.points[-1].Buffer(TOLERANCE)
-            
-            if not (buffer_a.Contains(artere.noeud_a.points[0]) and buffer_b.Contains(artere.noeud_b.points[0])) \
-               and not (buffer_a.Contains(artere.noeud_b.points[0]) and buffer_b.Contains(artere.noeud_a.points[0])):
-
-                self.log_error(MissingItem("Pas de noeud aux coordonnées attendues ('{}')".format(artere), filename=Artere.filename, field="geom"))
-        
-        
-        # Cables: comparer la géométrie à celle des equipements (on utilise en fait la geom du noeud correspondant à l'équipement)
-        for cable in cables:
-            if not cable.equipement_a or not cable.equipement_b or not cable.valid or not cable.equipement_a.noeud or not cable.equipement_b.noeud:
-                continue
-            
-            buffer_a, buffer_b = cable.points[0].Buffer(TOLERANCE), cable.points[-1].Buffer(TOLERANCE)
-            
-            if not (buffer_a.Contains(cable.equipement_a.noeud.points[0]) and buffer_b.Contains(cable.equipement_b.noeud.points[0])) \
-               and not (buffer_a.Contains(cable.equipement_b.noeud.points[0]) and buffer_b.Contains(cable.equipement_a.noeud.points[0])):
-            
-                self.log_error(MissingItem("Pas d'equipement aux coordonnées attendues ('{}')".format(cable), filename=Cable.filename, field="geom"))
-            
-        del buffer_a, buffer_b 
-        
-        # Verifie que chaque tranchée a au moins une artère
-        arteres_emprise = Feature.buffered_union(arteres, TOLERANCE)
-        
-        for tranchee in tranchees:
-            if not arteres_emprise.Contains(tranchee.geom):
-                self.log_error(MissingItem("Tranchée sans artère ('{}')".format(tranchee), filename=Tranchee.filename, field="-"))
-        
-        
-        # Verifie que chaque cable a au moins une artère (sauf si commentaire contient 'baguette')
-        for cable in cables:
-            if "baguette" in cable.CA_COMMENT.lower() or not cable.valid:
-                continue
-            if not arteres_emprise.Contains(cable.geom):
-                self.log_error(MissingItem("Cable sans artère ('{}')".format(cable), filename=Cable.filename, field="-"))
-        
-        del arteres_emprise
-        
-        # Verifie que chaque artère a au moins un cable (sauf si commentaire contient un de ces mots 'racco client adductio attente bus 'sans cable'')
-        cables_emprise = Feature.buffered_union(cables, TOLERANCE)
-        
-        for artere in arteres:
-            if any(x in artere.AR_COMMENT.lower() for x in ['racco','client','adductio','attente','bus','sans cable']):
-                continue
-            if not cables_emprise.Contains(artere.geom):
-                self.log_error(MissingItem("Artère sans cable ('{}')".format(artere), filename=Artere.filename, field="-"))
-                
-        del cables_emprise
-                
-        # Contrôle des dimensions logiques
-        for artere in arteres:
-            try:
-                if not int(artere.AR_FOU_DIS) <= int(artere.AR_NB_FOUR):
-                    self.log_error(DimensionError("Le nombre de fourreaux disponibles doit être inférieur au nombre total ('{}')".format(artere), filename=Artere.filename, field="AR_FOU_DIS"))
-            except (TypeError, ValueError):
-                pass
-        
-        for cable in cables:
-            try:
-                if not int(cable.CA_NB_FO_U) <= int(cable.CA_NB_FO):
-                    self.log_error(DimensionError("Le nombre de fourreaux utilisés doit être inférieur au nombre total ('{}')".format(cable), filename=Cable.filename, field="CA_NB_FO_U"))
-                if not int(cable.CA_NB_FO_D) <= int(cable.CA_NB_FO):
-                    self.log_error(DimensionError("Le nombre de fourreaux disponibles doit être inférieur au nombre total ('{}')".format(cable), filename=Cable.filename, field="CA_NB_FO_D"))
-            except (TypeError, ValueError):
-                pass
-        
-        
-        # Verifier que chaque equipement de type PBO est contenu dans une zapbo, et que le nom de la zapbo contient le nom de l'equipement
-        
-        for equipement in equipements.values():
-            if not equipement.EQ_TYPE == "PBO":
-                continue
-            
-            #zapbos englobant l'equipement
-            candidates = []
-            for zapbo in zapbos.values():
-                if zapbo.geom.Contains(equipement.geom):
-                    candidates.append(zapbo)
-                    
-            # le pbo doit être contenu dans une zapbo
-            if not candidates:
-                self.log_error(MissingItem("Le PBO n'est contenu dans aucune ZAPBO: {}".format(equipement), filename=Equipement.filename, field="geom"))
-                continue
-            
-            # On se base sur le nom pour trouver la zapbo correspondante
-            try:
-                equipement.zapbo = next((z for z in candidates if equipement.EQ_NOM in z.ID_ZAPBO))
-            except StopIteration:
-                self.log_error(MissingItem("Le nom du PBO ne coincide avec le nom d'aucune des ZAPBO qui le contient: {}".format(equipement), filename=Equipement.filename, field="EQ_NOM"))
-                break
-            
-            if equipement.zapbo.nb_prises is None:
-                equipement.zapbo.nb_prises = 0
-            
-            # Controle du dimensionnement des PBO
-            if equipement.EQ_TYPE_PH == 'PBO 6' and not equipement.zapbo.nb_prises < 6:
-                self.log_error(DimensionError("Le PBO 6 contient plus de 5 prises: {}".format(equipement), filename=Equipement.filename, field="-"))
-        
-            if equipement.EQ_TYPE_PH == 'PBO 12' and not equipement.zapbo.nb_prises >= 6 and equipement.zapbo.nb_prises <= 8:
-                self.log_error(DimensionError("Le PBO 12 contient mois de 6 prises ou plus de 8 prises: {}".format(equipement), filename=Equipement.filename, field="-"))
-        
-            if equipement.zapbo.STATUT == "REC" and not equipement.EQ_STATUT == "REC":
-                self.log_error(TechnicalValidationError("Le statut du PBO n'est pas cohérent avec le statut de sa ZAPBO: {}".format(equipement), filename=Equipement.filename, field="-"))
-        
-            if equipement.EQ_STATUT == "REC" and not equipement.zapbo.STATUT == "REC" and not equipement.zapbo.ID_ZAPBO[:4].lower() == "att_":
-                self.log_error(TechnicalValidationError("Le statut du PBO n'est pas cohérent avec le statut de sa ZAPBO: {}".format(equipement), filename=Equipement.filename, field="-"))
-        
-
-if __name__ == "__main__":
-    from core.constants import MAIN
-    subject = MAIN / "work" / "SOGETREL_026AP0_REC_181001_OK"
-    report = Netgeo112DoeValidator.submit(subject)
-    print(report)
-    

+ 450 - 0
schemas/mn2_rec.py

@@ -0,0 +1,450 @@
+'''
+
+@author: olivier.massot, 2018
+'''
+from osgeo.ogr import Feature
+
+from core import constants
+from core.cerberus_extend import is_float, is_multi_int, is_int, \
+    is_modern_french_date
+from core.validator import QgsModel, BaseValidator
+from core.validation_errors import UniqueError, InvalidGeometry, RelationError, \
+    DuplicatedGeom, MissingItem, DimensionError, TechnicalValidationError
+from schemas._common import TOLERANCE, XMIN, YMIN, XMAX, YMAX, CRS
+
+
+STATUTS = ['EN ETUDE', 'EN REALISATION', 'EN SERVICE', 'HORS SERVICE']
+
+
+##### Modeles
+
+
+class Artere(QgsModel):
+    layername = "artere_geo"
+    geom_type = constants.GEOM_LINE
+    crs = CRS
+    bounding_box = (XMIN,YMIN,XMAX,YMAX)
+    schema = {'AR_CODE': {'type': 'string', 'maxlength': 26},
+              'AR_NOM': {'type': 'string', 'maxlength': 26}, 
+              'AR_ID_INSE': {'type': 'string', 'empty': False, 'regex': r'50\d{3}'}, 
+              'AR_LONG': {'empty': False, 'validator': is_float},
+              'AR_ETAT': {'type': 'string', 'empty': False, 'allowed': ['0', '1', '2', '3', '4']}, 
+              'AR_OCCP': {'type': 'string', 'empty': False, 'allowed': ['0', '1.1', '1.2', '2', '3', '4']}, 
+              'AR_NOEUD_A': {'type': 'string', 'empty': False, 'maxlength': 20}, 
+              'AR_NOEUD_B': {'type': 'string', 'empty': False, 'maxlength': 20}, 
+              'AR_NB_FOUR': {'empty': False, 'validator': is_multi_int}, 
+              'AR_FOU_DIS': {'empty': False, 'validator': is_int}, 
+              'AR_TYPE_FO': {'type': 'string', 'multiallowed': ['PVC', 'PEHD', 'SOUS-TUBAGE PEHD', 'SOUS-TUBAGE  SOUPLE', 'FACADE', 'AERIEN', 'ENCORBELLEMENT', 'AUTRE']}, 
+              'AR_TYFO_AI': {'type': 'string', 'multiallowed': ['PVC', 'PEH', 'TUB', 'FAC', 'ENC', 'APP']}, 
+              'AR_DIAM_FO': {'type': 'string', 'multiallowed': ['10', '14', '18', '25', '28', '32', '40', '45', '60', '80', '150', 'NUL']}, 
+              'AR_PRO_FOU': {'type': 'string', 'multiallowed': ['MANCHE NUMERIQUE', 'COLLECTIVITE', 'ORANGE', 'PRIVE', 'ERDF', 'AUTRE (à préciser)']}, 
+              'AR_FABRICANT': {'type': 'string', 'empty': False, 'maxlength': 100},
+              'AR_REF_FABRICANT': {'type': 'string', 'empty': False, 'maxlength': 100},
+              'AR_COULEUR': {'type': 'string', 'empty': False, 'maxlength': 20},
+              'AR_AIGUILLEE': {'type': 'string', 'empty': False, 'maxlength': 3, 'allowed': ['OUI', 'NON']},
+              'AR_NB_CABLES': {'empty': False, 'validator': is_int},
+              'AR_PRO_CAB': {'type': 'string', 'empty': False, 'allowed': ['MANCHE NUMERIQUE']}, 
+              'AR_GEST_FO': {'type': 'string', 'multiallowed': ['MANCHE NUMERIQUE', 'MANCHE TELECOM', 'COLLECTIVITE', 'ORANGE', 'MANCHE FIBRE', 'PRIVE', 'ERDF', 'AUTRE (à préciser)', 'NUL']}, 
+              'AR_UTIL_FO': {'type': 'string', 'multiallowed': ['MANCHE NUMERIQUE', 'MANCHE TELECOM', 'COLLECTIVITE', 'ORANGE', 'MANCHE FIBRE', 'PRIVE', 'AUTRE (à préciser)', 'NUL']}, 
+              'AR_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
+              'AR_DATE_RE': {'empty': False, 'validator': is_modern_french_date}, 
+              'AR_REF_PLA': {'type': 'string', 'maxlength': 100}, 
+              'AR_SRC_GEO': {'type': 'string', 'maxlength': 50}, 
+              'AR_QLT_GEO': {'type': 'string', 'empty': False, 'allowed': ['A', 'B', 'C']}, 
+              'AR_PRO_MD': {'type': 'string', 'empty': False, 'default': 'MANCHE NUMERIQUE', 'allowed': ['MANCHE NUMERIQUE']}, 
+              'AR_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
+              'AR_STATUT': {'type': 'string', 'empty': False, 'allowed': STATUTS}}
+
+    def __repr__(self):
+        return "Artere {}".format(self.AR_CODE)
+
+class Cable(QgsModel):
+    layername = "cable_geo"
+    geom_type = constants.GEOM_LINE
+    crs = CRS
+    bounding_box = (XMIN,YMIN,XMAX,YMAX)
+    schema = {'CA_CODE': {'type': 'string', 'maxlength': 18}, 
+              'CA_NOM': {'type': 'string', 'maxlength': 18}, 
+              'CA_NUMERO': {'type': 'string', 'maxlength': 17},  
+              'CA_EMPRISE': {'type': 'string', 'maxlength': 10}, 
+              'CA_FAB': {'type': 'string', 'maxlength': 100}, 
+              'CA_REFFAB': {'type': 'string', 'maxlength': 100}, 
+              'CA_MODELE': {'type': 'string', 'maxlength': 10}, 
+              'CA_TYPE': {'type': 'string', 'maxlength': 10, 'empty': False, 'allowed': ['AERIEN', 'IMMEUBLE', 'FACADE', 'MIXTE', 'SOUTERRAIN']},
+              'CA_TYPFCT': {'type': 'string', 'maxlength': 3, 'empty': False, 'allowed': ['CDI', 'CTR', 'CBM', 'RAC', 'CBO']},
+              'CA_ETAT': {'type': 'string', 'maxlength': 1, 'empty': False, 'allowed': ['0', '1', '2', '3', '4']}, 
+              'CA_LONG': {'validator': is_float}, 
+              'CA_EQ_A': {'type': 'string', 'maxlength': 18}, 
+              'CA_EQ_B': {'type': 'string', 'maxlength': 18}, 
+              'CA_DIAMETR': {'empty': False, 'validator': is_float}, 
+              'CA_COULEUR': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['NOIR', 'BLEU', 'BLANC']}, 
+              'CA_TECHNOL': {'type': 'string', 'maxlength': 17, 'empty': False, 'allowed': ['G657A2_M6', 'G657A2_M12']}, 
+              'CA_NB_FO': {'validator': is_int}, 
+              'CA_NB_FO_U': {'empty': False, 'validator': is_int}, 
+              'CA_NB_FO_D': {'empty': False, 'validator': is_int}, 
+              'CA_PRO': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE']}, 
+              'CA_GEST': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE FIBRE']}, 
+              'CA_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
+              'CA_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
+              'CA_STATUT': {'type': 'string', 'empty': False, 'allowed': STATUTS}}
+
+    def __repr__(self):
+        return "Cable {}-{}".format(self.CA_EQ_A, self.CA_EQ_B)
+    
+class Equipement(QgsModel):
+    layername = "equipement_passif"
+    geom_type = constants.GEOM_POINT
+    crs = CRS
+    bounding_box = (XMIN,YMIN,XMAX,YMAX)
+    schema = {'EQ_CODE': {'type': 'string', 'maxlength': 18}, 
+              'EQ_NOM': {'type': 'string', 'maxlength': 10, 'contains_any_of': ['PBO', 'BPE', 'BAI']}, 
+              'EQ_NOM_NOE': {'type': 'string', 'maxlength': 30}, 
+              'EQ_REF': {'type': 'string', 'maxlength': 100}, 
+              'EQ_EMPRISE': {'type': 'string', 'maxlength': 7}, 
+              'EQ_FABR': {'type': 'string', 'maxlength': 100}, 
+              'EQ_CAPFO': {'empty': False, 'validator': is_int}, 
+              'EQ_NBMXEQ': {'empty': False, 'validator': is_int}, 
+              'EQ_NBCAB': {'empty': False, 'validator': is_int}, 
+              'EQ_DIMENS': {'type': 'string', 'maxlength': 50}, 
+              'EQ_TYPEQ': {'type': 'string', 'maxlength': 100}, 
+              'EQ_ETAT': {'type': 'string', 'empty': False, 'allowed': ['0', '1', '2', '3', '4']}, 
+              'EQ_OCCP': {'type': 'string', 'empty': False, 'allowed': ['0', '1.1', '1.2', '2', '3', '4']}, 
+              'EQ_TYPE': {'type': 'string', 'empty': False, 'allowed': ['PBO', 'PBOE', 'BPE', 'BAI']},
+              'EQ_TYPSTRC': {'type': 'string', 'empty': False, 'allowed': ['CHAMBRE', 'AERIEN', 'FACADE', 'COLONNE MONTANTE', 'PIED IMMEUBLE', 'DTIO']}, 
+              'EQ_TYPE_LQ': {'type': 'string', 'empty': False, 'allowed': ['PBO', 'BPE JB', 'BPE JD', 'BAIDC', 'BAIOP']}, 
+              'EQ_TYPE_PH': {'type': 'string', 'empty': False, 'allowed': ['PBO 6', 'PBO 12', 'BPE 12EP', 'BPE 24EP', 'BPE 48EP', 'BPE 72EP', 'BPE 96EP', 'BPE 144EP', 'BPE 288EP', 'BPE 576EP', 'BPE 720EP', 'BAI']}, 
+              'EQ_PRO': {'type': 'string', 'empty': False, 'allowed': ['MANCHE NUMERIQUE', 'COLLECTIVITE', 'ORANGE', 'PRIVE', 'AUTRE (à préciser)', 'NUL']}, 
+              'EQ_GEST': {'type': 'string', 'empty': False, 'allowed': ['MANCHE NUMERIQUE', 'MANCHE TELECOM', 'COLLECTIVITE', 'ORANGE', 'MANCHE FIBRE', 'PRIVE', 'AUTRE (à préciser)', 'NUL']}, 
+              'EQ_HAUT': {'empty': False, 'validator': is_float}, 
+              'EQ_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
+              'EQ_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
+              'EQ_STATUT': {'type': 'string', 'empty': False, 'allowed': STATUTS}}
+        
+    def __repr__(self):
+        return "Equipement {}".format(self.EQ_NOM)
+
+class Noeud(QgsModel):
+    layername = "noeud_geo"
+    geom_type = constants.GEOM_POINT
+    crs = CRS
+    bounding_box = (XMIN,YMIN,XMAX,YMAX)
+    schema = {'NO_CODE': {'type': 'string', 'maxlength': 18}, 
+              'NO_NOM': {'type': 'string', 'maxlength': 30}, 
+              'NO_ID_INSE': {'type': 'string', 'empty': False, 'regex': r'50\d{3}'}, 
+              'NO_VOIE': {'type': 'string', 'maxlength': 100}, 
+              'NO_EMPRISE': {'type': 'string', 'maxlength': 10}, 
+              'NO_ETAT': {'type': 'string', 'empty': False, 'allowed': ['0', '1', '2', '3', '4']}, 
+              'NO_OCCP': {'type': 'string', 'empty': False, 'allowed': ['0', '1.1', '1.2', '2', '3', '4']}, 
+              'NO_TYPE': {'type': 'string', 'empty': False, 'allowed': ['CHA', 'POT', 'LTE', 'SEM', 'FAC', 'OUV', 'IMM']}, 
+              'NO_TYPFCT': {'type': 'string', 'empty': False, 'allowed': ['INTERCONNEXION', 'SATELLITE', 'PASSAGE', 'REGARD', 'INDETERMINE']}, 
+              'NO_TYPE_LQ': {'type': 'string', 'empty': False, 'allowed': ['CHTIR', 'CHRACC', 'POT', 'NRO', 'PM', 'MIMO', 'FAC', 'OUV', 'IMM']}, 
+              'NO_TYPE_PH': {'type': 'string', 'empty': False, 'allowed': ['CHAMBRE', 'POTEAU', 'ARMOIRE', 'SHELTER', 'BATIMENT', 'SITE MIMO', 'FACADE', 'OUVRAGE', 'IMMEUBLE']}, 
+              'NO_CODE_PH': {'type': 'string', 'maxlength': 20}, 
+              'NO_TECH_PS': {'type': 'string', 'maxlength': 20, 'multiallowed': ['COAX', 'CUT', 'ECL', 'ELEC', 'VP', 'OPT', 'NC']}, 
+              'NO_AMO': {'type': 'string', 'maxlength': 20}, 
+              'NO_PLINOX': {'required':False, 'type': 'string', 'maxlength': 3, 'allowed': ['OUI', 'NON']}, 
+              'NO_X': {'empty': False, 'validator': is_float}, 
+              'NO_Y': {'empty': False, 'validator': is_float}, 
+              'NO_PRO': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE', 'COLLECTIVITE', 'ORANGE', 'ERDF', 'PRIVE', 'ENEDIS', 'AUTRE (à préciser)', 'NUL']}, 
+              'NO_GEST': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE', 'MANCHE TELECOM', 'COLLECTIVITE', 'ORANGE', 'ERDF', 'ENEDIS', 'MANCHE FIBRE', 'PRIVE', 'AUTRE (à préciser)', 'NUL']}, 
+              'NO_HAUT': {'empty': False, 'validator': is_float}, 
+              'NO_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
+              'NO_REF_PLA': {'type': 'string', 'maxlength': 100}, 
+              'NO_SRC_GEO': {'type': 'string', 'maxlength': 50}, 
+              'NO_QLT_GEO': {'type': 'string', 'maxlength': 1, 'empty': False, 'allowed': ['A', 'B', 'C']}, 
+              'NO_PRO_MD': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE']}, 
+              'NO_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
+              'NO_STATUT': {'type': 'string', 'empty': False, 'allowed': STATUTS}}
+
+    def __repr__(self):
+        return "Noeud {}".format(self.NO_NOM)
+    
+class Tranchee(QgsModel):
+    layername = "tranchee_geo"
+    geom_type = constants.GEOM_LINE
+    crs = CRS
+    bounding_box = (XMIN,YMIN,XMAX,YMAX)
+    schema = {'TR_CODE': {'type': 'string', 'maxlength': 23}, 
+              'TR_NOM': {'type': 'string', 'maxlength': 23}, 
+              'TR_ID_INSE': {'type': 'string', 'empty': False, 'regex': r'50\d{3}'}, 
+              'TR_VOIE': {'type': 'string', 'maxlength': 200}, 
+              'TR_TYP_IMP': {'type': 'string', 'empty': False, 'allowed': ['ACCOTEMENT STABILISE', 'ACCOTEMENT NON STABILISE', 'CHAUSSEE LOURDE', 'CHAUSSEE LEGERE', 'FOSSE', 'TROTTOIR', 'ESPACE VERT', 'ENCORBELLEMENT']}, 
+              'TR_MOD_POS': {'type': 'string', 'empty': False, 'allowed': ['TRADITIONNEL', 'MICRO TRANCHEE', 'FONCAGE 60', 'FONCAGE 90', 'FONCAGE 120', 'TRANCHEUSE', 'FORAGE URBAIN', 'FORAGE RURAL', 'ENCORBELLEMENT']}, 
+              'TR_MPOS_AI': {'type': 'string', 'empty': False, 'allowed': ['TRA', 'ALL', 'FONCAGE 60', 'FON', 'FOR', 'ENC']}, 
+              'TR_LONG': {'empty': False, 'validator': is_float}, 
+              'TR_LARG': {'empty': False, 'validator': is_float}, 
+              'TR_REVET': {'empty':True, 'type': 'string', 'allowed': ['SABLE', 'BICOUCHE', 'ENROBE', 'BETON', 'PAVE', 'TERRAIN NATUREL']}, 
+              'TR_CHARGE': {'empty': False, 'validator': is_float}, 
+              'TR_GRILLAG': {'empty':True, 'validator': is_float}, 
+              'TR_REMBLAI': {'type': 'string'}, 
+              'TR_PLYNOX': {'type': 'string', 'empty': False, 'allowed': ['OUI', 'NON']}, 
+              'TR_PRO_VOI': {'type': 'string', 'empty': False, 'allowed': ['COMMUNE', 'COMMUNAUTE DE COMMUNES', 'DEPARTEMENT', 'ETAT', 'PRIVE']}, 
+              'TR_GEST_VO': {'type': 'string', 'empty': False, 'allowed': ['COMMUNE', 'COMMUNAUTE DE COMMUNES', 'DEPARTEMENT', 'ETAT', 'PRIVE']}, 
+              'TR_SCHEMA': {'maxlength': 100, 'type': 'string'}, 
+              'TR_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
+              'TR_SRC_GEO': {'type': 'string', 'maxlength': 50}, 
+              'TR_QLT_GEO': {'type': 'string', 'empty': False, 'allowed': ['A', 'B', 'C']}, 
+              'TR_PRO_MD': {'type': 'string', 'maxlength': 20}, 
+              'TR_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
+              'TR_STATUT': {'type': 'string', 'empty': False, 'allowed': STATUTS}}
+
+    def __repr__(self):
+        return "Tranchee {}".format(self.TR_VOIE)
+    
+class Zapbo(QgsModel):
+    layername = "zapbo_geo"
+    geom_type = constants.GEOM_POLYGON
+    crs = CRS
+    bounding_box = (XMIN,YMIN,XMAX,YMAX)
+    schema = {'ID_ZAPBO': {'type': 'string', 'maxlength': 30, 'contains_any_of': ['PBO', 'BPE']}, 
+              'COMMENTAIR': {'type': 'string', 'maxlength': 254, 'empty': True}, 
+              'STATUT': {'type': 'string', 'empty': False, 'allowed': STATUTS}}
+    
+    def __repr__(self):
+        return "Zapbo {}".format(self.ID_ZAPBO)
+
+
+
+
+##### Validator
+
+
+class Validator(BaseValidator):
+    schema_name = "MN2 REC"
+    models = [Artere, Cable, Equipement, Noeud, Tranchee, Zapbo]
+    
+    def _technical_validation(self):
+        
+        # construction des index
+        arteres = self.dataset[Artere]
+        cables = self.dataset[Cable]
+        tranchees = self.dataset[Tranchee]
+        
+        noeuds = {}
+        for noeud in self.dataset[Noeud]:
+            if not noeud.NO_NOM in noeuds:
+                noeuds[noeud.NO_NOM] = noeud
+            else:
+                self.log_error(UniqueError("Doublons dans le champs: {}".format(noeud), layername=Noeud.layername, field="NO_NOM"))
+        
+        equipements = {}
+        for equipement in self.dataset[Equipement]:
+            if not equipement.EQ_NOM in equipements:
+                equipements[equipement.EQ_NOM] = equipement
+            else:
+                self.log_error(UniqueError("Doublons dans le champs: {}".format(equipement), layername=Equipement.layername, field="EQ_NOM"))
+                
+        zapbos = {}
+        for zapbo in self.dataset[Zapbo]:
+            if not zapbo.ID_ZAPBO in zapbos:
+                zapbos[zapbo.ID_ZAPBO] = zapbo
+            else:
+                self.log_error(UniqueError("Doublons dans le champs: {}".format(zapbo), layername=Zapbo.layername, field="ID_ZAPBO"))
+        
+        # contrôle de la validité des géométries
+        for artere in arteres:
+            if not artere.valid:
+                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(artere), layername=Artere.layername, field="geom"))
+        for tranchee in tranchees:
+            if not tranchee.valid:
+                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(tranchee), layername=Tranchee.layername, field="geom"))
+        for cable in cables:
+            if not "baguette" in cable.CA_COMMENT.lower() and not cable.valid:
+                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(cable), layername=Cable.layername, field="geom"))
+        for noeud in noeuds.values():
+            if not noeud.valid:
+                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(noeud), layername=Noeud.layername, field="geom"))
+        for equipement in equipements.values():
+            if not equipement.valid:
+                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(equipement), layername=Equipement.layername, field="geom"))
+        for zapbo in zapbos.values():
+            if not zapbo.valid:
+                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(zapbo), layername=Zapbo.layername, field="geom"))
+        
+        # rattachement les noeuds aux artères     
+        for artere in arteres:
+            try:
+                artere.noeud_a = noeuds[artere.AR_NOEUD_A]
+            except KeyError:
+                artere.noeud_a = None
+                self.log_error(RelationError("Le noeud '{}' n'existe pas".format(artere.AR_NOEUD_A), layername=Artere.layername, field="AR_NOEUD_A"))
+                
+            try:
+                artere.noeud_b = noeuds[artere.AR_NOEUD_B]
+            except KeyError:
+                artere.noeud_b = None
+                self.log_error(RelationError("Le noeud '{}' n'existe pas".format(artere.AR_NOEUD_B), layername=Artere.layername, field="AR_NOEUD_A"))
+        
+        # rattachement des equipements aux cables
+        for cable in cables:
+            try:
+                cable.equipement_a = equipements[cable.CA_EQ_A]
+            except KeyError:
+                cable.equipement_a = None
+                self.log_error(RelationError("L'équipement '{}' n'existe pas".format(cable.CA_EQ_A), layername=Cable.layername, field="CA_EQ_A"))
+                
+            try:
+                cable.equipement_b = equipements[cable.CA_EQ_B]
+            except KeyError:
+                cable.equipement_b = None
+                self.log_error(RelationError("L'équipement '{}' n'existe pas".format(cable.CA_EQ_B), layername=Cable.layername, field="CA_EQ_B"))
+
+        # rattachement des equipements aux noeuds
+        for equipement in equipements.values():
+            try:
+                equipement.noeud = noeuds[equipement.EQ_NOM_NOE]
+            except KeyError:
+                equipement.noeud = None
+                self.log_error(RelationError("Le noeud '{}' n'existe pas".format(equipement.EQ_NOM_NOE, equipement.EQ_NOM), layername=Equipement.layername, field="EQ_NOM_NOE"))
+
+        # verifie que tous les equipements sont l'equipement B d'au moins un cable
+        equipements_b = [cable.CA_EQ_B for cable in cables]
+        for eq_id in equipements:
+            if equipements[eq_id].EQ_TYPE == "BAI":
+                continue
+            if not eq_id in equipements_b:
+                self.log_error(RelationError("L'equipement '{}' n'est l'équipement B d'aucun cable".format(eq_id), layername=Equipement.layername, field="EQ_NOM"))
+
+        # controle des doublons graphiques
+        for i, tranchee in enumerate(tranchees):
+            for other in tranchees[i+1:]:
+                if tranchee.geom == other.geom:
+                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée".format(tranchee), layername=Tranchee.layername, field="geom"))
+                    
+        for i, artere in enumerate(arteres):
+            for other in arteres[i+1:]:
+                if artere.geom == other.geom:
+                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('{}')".format(artere), layername=Artere.layername, field="geom"))
+
+        for i, cable in enumerate(cables):
+            for other in cables[i+1:]:
+                if cable.geom == other.geom and cable.CA_EQ_A == other.CA_EQ_A and cable.CA_EQ_B == other.CA_EQ_B:
+                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('{}')".format(cable), layername=Cable.layername, field="geom"))
+        
+        ls_noeuds = list(noeuds.values())
+        for i, noeud in enumerate(ls_noeuds):
+            for other in ls_noeuds[i+1:]:
+                if noeud.geom == other.geom:
+                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('{}')".format(noeud), layername=Noeud.layername, field="geom"))
+        del ls_noeuds
+        
+        ls_zapbos = list(zapbos.values())
+        for i, zapbo in enumerate(ls_zapbos):
+            for other in ls_zapbos[i+1:]:
+                if zapbo.geom == other.geom:
+                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('{}')".format(zapbo), layername=Zapbo.layername, field="geom"))
+        del ls_zapbos
+           
+        # Arteres: comparer la géométrie à celle des noeuds
+        for artere in arteres:
+            if not artere.noeud_a or not artere.noeud_b:
+                continue
+            
+            buffer_a, buffer_b = artere.points[0].Buffer(TOLERANCE), artere.points[-1].Buffer(TOLERANCE)
+            
+            if not (buffer_a.Contains(artere.noeud_a.points[0]) and buffer_b.Contains(artere.noeud_b.points[0])) \
+               and not (buffer_a.Contains(artere.noeud_b.points[0]) and buffer_b.Contains(artere.noeud_a.points[0])):
+
+                self.log_error(MissingItem("Pas de noeud aux coordonnées attendues ('{}')".format(artere), layername=Artere.layername, field="geom"))
+        
+        
+        # Cables: comparer la géométrie à celle des equipements (on utilise en fait la geom du noeud correspondant à l'équipement)
+        for cable in cables:
+            if not cable.equipement_a or not cable.equipement_b or not cable.valid or not cable.equipement_a.noeud or not cable.equipement_b.noeud:
+                continue
+            
+            buffer_a, buffer_b = cable.points[0].Buffer(TOLERANCE), cable.points[-1].Buffer(TOLERANCE)
+            
+            if not (buffer_a.Contains(cable.equipement_a.noeud.points[0]) and buffer_b.Contains(cable.equipement_b.noeud.points[0])) \
+               and not (buffer_a.Contains(cable.equipement_b.noeud.points[0]) and buffer_b.Contains(cable.equipement_a.noeud.points[0])):
+            
+                self.log_error(MissingItem("Pas d'equipement aux coordonnées attendues ('{}')".format(cable), layername=Cable.layername, field="geom"))
+            
+        del buffer_a, buffer_b 
+        
+        # Verifie que chaque tranchée a au moins une artère
+        arteres_emprise = Feature.buffered_union(arteres, TOLERANCE)
+        
+        for tranchee in tranchees:
+            if not arteres_emprise.Contains(tranchee.geom):
+                self.log_error(MissingItem("Tranchée sans artère ('{}')".format(tranchee), layername=Tranchee.layername, field="-"))
+        
+        
+        # Verifie que chaque cable a au moins une artère (sauf si commentaire contient 'baguette')
+        for cable in cables:
+            if "baguette" in cable.CA_COMMENT.lower() or not cable.valid:
+                continue
+            if not arteres_emprise.Contains(cable.geom):
+                self.log_error(MissingItem("Cable sans artère ('{}')".format(cable), layername=Cable.layername, field="-"))
+        
+        del arteres_emprise
+        
+        # Verifie que chaque artère a au moins un cable (sauf si commentaire contient un de ces mots 'racco client adductio attente bus 'sans cable'')
+        cables_emprise = Feature.buffered_union(cables, TOLERANCE)
+        
+        for artere in arteres:
+            if any(x in artere.AR_COMMENT.lower() for x in ['racco','client','adductio','attente','bus','sans cable']):
+                continue
+            if not cables_emprise.Contains(artere.geom):
+                self.log_error(MissingItem("Artère sans cable ('{}')".format(artere), layername=Artere.layername, field="-"))
+                
+        del cables_emprise
+                
+        # Contrôle des dimensions logiques
+        for artere in arteres:
+            try:
+                if not int(artere.AR_FOU_DIS) <= int(artere.AR_NB_FOUR):
+                    self.log_error(DimensionError("Le nombre de fourreaux disponibles doit être inférieur au nombre total ('{}')".format(artere), layername=Artere.layername, field="AR_FOU_DIS"))
+            except (TypeError, ValueError):
+                pass
+        
+        for cable in cables:
+            try:
+                if not int(cable.CA_NB_FO_U) <= int(cable.CA_NB_FO):
+                    self.log_error(DimensionError("Le nombre de fourreaux utilisés doit être inférieur au nombre total ('{}')".format(cable), layername=Cable.layername, field="CA_NB_FO_U"))
+                if not int(cable.CA_NB_FO_D) <= int(cable.CA_NB_FO):
+                    self.log_error(DimensionError("Le nombre de fourreaux disponibles doit être inférieur au nombre total ('{}')".format(cable), layername=Cable.layername, field="CA_NB_FO_D"))
+            except (TypeError, ValueError):
+                pass
+        
+        # Verifier que chaque equipement de type PBO est contenu dans une zapbo, et que le nom de la zapbo contient le nom de l'equipement
+        
+        for equipement in equipements.values():
+            if not equipement.EQ_TYPE == "PBO":
+                continue
+            
+            #zapbos englobant l'equipement
+            candidates = []
+            for zapbo in zapbos.values():
+                if zapbo.geom.Contains(equipement.geom):
+                    candidates.append(zapbo)
+                    
+            # le pbo doit être contenu dans une zapbo
+            if not candidates:
+                self.log_error(MissingItem("Le PBO n'est contenu dans aucune ZAPBO: {}".format(equipement), layername=Equipement.layername, field="geom"))
+                continue
+            
+            # On se base sur le nom pour trouver la zapbo correspondante
+            try:
+                equipement.zapbo = next((z for z in candidates if equipement.EQ_NOM in z.ID_ZAPBO))
+            except StopIteration:
+                self.log_error(MissingItem("Le nom du PBO ne coincide avec le nom d'aucune des ZAPBO qui le contient: {}".format(equipement), layername=Equipement.layername, field="EQ_NOM"))
+                break
+            
+            if equipement.zapbo.nb_prises is None:
+                equipement.zapbo.nb_prises = 0
+            
+            # Controle du dimensionnement des PBO
+            if equipement.zapbo.nb_prises is not None:
+                if equipement.EQ_TYPE_PH == 'PBO 6' and not equipement.zapbo.nb_prises < 6:
+                    self.log_error(DimensionError("Le PBO 6 contient plus de 5 prises: {}".format(equipement), layername=Equipement.layername, field="-"))
+            
+                if equipement.EQ_TYPE_PH == 'PBO 12' and not equipement.zapbo.nb_prises >= 6 and equipement.zapbo.nb_prises <= 8:
+                    self.log_error(DimensionError("Le PBO 12 contient mois de 6 prises ou plus de 8 prises: {}".format(equipement), layername=Equipement.layername, field="-"))
+        
+            if equipement.zapbo.STATUT == "REC" and not equipement.EQ_STATUT == "REC":
+                self.log_error(TechnicalValidationError("Le statut du PBO n'est pas cohérent avec le statut de sa ZAPBO: {}".format(equipement), layername=Equipement.layername, field="-"))
+        
+            if equipement.EQ_STATUT == "REC" and not equipement.zapbo.STATUT == "REC" and not equipement.zapbo.ID_ZAPBO[:4].lower() == "att_":
+                self.log_error(TechnicalValidationError("Le statut du PBO n'est pas cohérent avec le statut de sa ZAPBO: {}".format(equipement), layername=Equipement.layername, field="-"))
+        

+ 0 - 4
schemas/mn2_rec/__init__.py

@@ -1,4 +0,0 @@
-from schemas.mn2_rec.validator import Mn2RecValidator
-
-
-validator = Mn2RecValidator

+ 0 - 189
schemas/mn2_rec/models.py

@@ -1,189 +0,0 @@
-'''
-
-@author: olivier.massot, 2018
-'''
-from core import gis_
-from core.cerberus_extend import is_int, is_float, \
-    is_modern_french_date, is_multi_int
-from core.validation import BaseGeoModel
-from schemas.common import XMIN, YMIN, XMAX, YMAX
-
-STATUTS = ['EN ETUDE', 'EN REALISATION', 'EN SERVICE', 'HORS SERVICE']
-
-class Artere(BaseGeoModel):
-    filename = "artere_geo.shp"
-    geom_type = gis_.GEOM_LINE
-    bounding_box = (XMIN,YMIN,XMAX,YMAX)
-    schema = {'AR_CODE': {'type': 'string', 'maxlength': 26},
-              'AR_NOM': {'type': 'string', 'maxlength': 26}, 
-              'AR_ID_INSE': {'type': 'string', 'empty': False, 'regex': r'50\d{3}'}, 
-              'AR_LONG': {'empty': False, 'validator': is_float},
-              'AR_ETAT': {'type': 'string', 'empty': False, 'allowed': ['0', '1', '2', '3', '4']}, 
-              'AR_OCCP': {'type': 'string', 'empty': False, 'allowed': ['0', '1.1', '1.2', '2', '3', '4']}, 
-              'AR_NOEUD_A': {'type': 'string', 'empty': False, 'maxlength': 20}, 
-              'AR_NOEUD_B': {'type': 'string', 'empty': False, 'maxlength': 20}, 
-              'AR_NB_FOUR': {'empty': False, 'validator': is_multi_int}, 
-              'AR_FOU_DIS': {'empty': False, 'validator': is_int}, 
-              'AR_TYPE_FO': {'type': 'string', 'multiallowed': ['PVC', 'PEHD', 'SOUS-TUBAGE PEHD', 'SOUS-TUBAGE  SOUPLE', 'FACADE', 'AERIEN', 'ENCORBELLEMENT', 'AUTRE']}, 
-              'AR_TYFO_AI': {'type': 'string', 'multiallowed': ['PVC', 'PEH', 'TUB', 'FAC', 'ENC', 'APP']}, 
-              'AR_DIAM_FO': {'type': 'string', 'multiallowed': ['10', '14', '18', '25', '28', '32', '40', '45', '60', '80', '150', 'NUL']}, 
-              'AR_PRO_FOU': {'type': 'string', 'multiallowed': ['MANCHE NUMERIQUE', 'COLLECTIVITE', 'ORANGE', 'PRIVE', 'ERDF', 'AUTRE (à préciser)']}, 
-              'AR_FABRICANT': {'type': 'string', 'empty': False, 'maxlength': 100},
-              'AR_REF_FABRICANT': {'type': 'string', 'empty': False, 'maxlength': 100},
-              'AR_COULEUR': {'type': 'string', 'empty': False, 'maxlength': 20},
-              'AR_AIGUILLEE': {'type': 'string', 'empty': False, 'maxlength': 3, 'allowed': ['OUI', 'NON']},
-              'AR_NB_CABLES': {'empty': False, 'validator': is_int},
-              'AR_PRO_CAB': {'type': 'string', 'empty': False, 'allowed': ['MANCHE NUMERIQUE']}, 
-              'AR_GEST_FO': {'type': 'string', 'multiallowed': ['MANCHE NUMERIQUE', 'MANCHE TELECOM', 'COLLECTIVITE', 'ORANGE', 'MANCHE FIBRE', 'PRIVE', 'ERDF', 'AUTRE (à préciser)', 'NUL']}, 
-              'AR_UTIL_FO': {'type': 'string', 'multiallowed': ['MANCHE NUMERIQUE', 'MANCHE TELECOM', 'COLLECTIVITE', 'ORANGE', 'MANCHE FIBRE', 'PRIVE', 'AUTRE (à préciser)', 'NUL']}, 
-              'AR_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
-              'AR_DATE_RE': {'empty': False, 'validator': is_modern_french_date}, 
-              'AR_REF_PLA': {'type': 'string', 'maxlength': 100}, 
-              'AR_SRC_GEO': {'type': 'string', 'maxlength': 50}, 
-              'AR_QLT_GEO': {'type': 'string', 'empty': False, 'allowed': ['A', 'B', 'C']}, 
-              'AR_PRO_MD': {'type': 'string', 'empty': False, 'default': 'MANCHE NUMERIQUE', 'allowed': ['MANCHE NUMERIQUE']}, 
-              'AR_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
-              'AR_STATUT': {'type': 'string', 'empty': False, 'allowed': STATUTS}}
-
-    def __repr__(self):
-        return "Artere {}".format(self.AR_CODE)
-
-class Cable(BaseGeoModel):
-    filename = "cable_geo.shp"
-    geom_type = gis_.GEOM_LINE
-    bounding_box = (XMIN,YMIN,XMAX,YMAX)
-    schema = {'CA_CODE': {'type': 'string', 'maxlength': 18}, 
-              'CA_NOM': {'type': 'string', 'maxlength': 18}, 
-              'CA_NUMERO': {'type': 'string', 'maxlength': 17},  
-              'CA_EMPRISE': {'type': 'string', 'maxlength': 10}, 
-              'CA_FAB': {'type': 'string', 'maxlength': 100}, 
-              'CA_REFFAB': {'type': 'string', 'maxlength': 100}, 
-              'CA_MODELE': {'type': 'string', 'maxlength': 10}, 
-              'CA_TYPE': {'type': 'string', 'maxlength': 10, 'empty': False, 'allowed': ['AERIEN', 'IMMEUBLE', 'FACADE', 'MIXTE', 'SOUTERRAIN']},
-              'CA_TYPFCT': {'type': 'string', 'maxlength': 3, 'empty': False, 'allowed': ['CDI', 'CTR', 'CBM', 'RAC', 'CBO']},
-              'CA_ETAT': {'type': 'string', 'maxlength': 1, 'empty': False, 'allowed': ['0', '1', '2', '3', '4']}, 
-              'CA_LONG': {'validator': is_float}, 
-              'CA_EQ_A': {'type': 'string', 'maxlength': 18}, 
-              'CA_EQ_B': {'type': 'string', 'maxlength': 18}, 
-              'CA_DIAMETR': {'empty': False, 'validator': is_float}, 
-              'CA_COULEUR': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['NOIR', 'BLEU', 'BLANC']}, 
-              'CA_TECHNOL': {'type': 'string', 'maxlength': 17, 'empty': False, 'allowed': ['G657A2_M6', 'G657A2_M12']}, 
-              'CA_NB_FO': {'validator': is_int}, 
-              'CA_NB_FO_U': {'empty': False, 'validator': is_int}, 
-              'CA_NB_FO_D': {'empty': False, 'validator': is_int}, 
-              'CA_PRO': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE']}, 
-              'CA_GEST': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE FIBRE']}, 
-              'CA_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
-              'CA_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
-              'CA_STATUT': {'type': 'string', 'empty': False, 'allowed': STATUTS}}
-
-    def __repr__(self):
-        return "Cable {}-{}".format(self.CA_EQ_A, self.CA_EQ_B)
-    
-class Equipement(BaseGeoModel):
-    filename = "equipement_passif.shp"
-    geom_type = gis_.GEOM_POINT
-    bounding_box = (XMIN,YMIN,XMAX,YMAX)
-    schema = {'EQ_CODE': {'type': 'string', 'maxlength': 18}, 
-              'EQ_NOM': {'type': 'string', 'maxlength': 10, 'contains_any_of': ['PBO', 'BPE', 'BAI']}, 
-              'EQ_NOM_NOE': {'type': 'string', 'maxlength': 30}, 
-              'EQ_REF': {'type': 'string', 'maxlength': 100}, 
-              'EQ_EMPRISE': {'type': 'string', 'maxlength': 7}, 
-              'EQ_FABR': {'type': 'string', 'maxlength': 100}, 
-              'EQ_CAPFO': {'empty': False, 'validator': is_int}, 
-              'EQ_NBMXEQ': {'empty': False, 'validator': is_int}, 
-              'EQ_NBCAB': {'empty': False, 'validator': is_int}, 
-              'EQ_DIMENS': {'type': 'string', 'maxlength': 50}, 
-              'EQ_TYPEQ': {'type': 'string', 'maxlength': 100}, 
-              'EQ_ETAT': {'type': 'string', 'empty': False, 'allowed': ['0', '1', '2', '3', '4']}, 
-              'EQ_OCCP': {'type': 'string', 'empty': False, 'allowed': ['0', '1.1', '1.2', '2', '3', '4']}, 
-              'EQ_TYPE': {'type': 'string', 'empty': False, 'allowed': ['PBO', 'PBOE', 'BPE', 'BAI']},
-              'EQ_TYPSTRC': {'type': 'string', 'empty': False, 'allowed': ['CHAMBRE', 'AERIEN', 'FACADE', 'COLONNE MONTANTE', 'PIED IMMEUBLE', 'DTIO']}, 
-              'EQ_TYPE_LQ': {'type': 'string', 'empty': False, 'allowed': ['PBO', 'BPE JB', 'BPE JD', 'BAIDC', 'BAIOP']}, 
-              'EQ_TYPE_PH': {'type': 'string', 'empty': False, 'allowed': ['PBO 6', 'PBO 12', 'BPE 12EP', 'BPE 24EP', 'BPE 48EP', 'BPE 72EP', 'BPE 96EP', 'BPE 144EP', 'BPE 288EP', 'BPE 576EP', 'BPE 720EP', 'BAI']}, 
-              'EQ_PRO': {'type': 'string', 'empty': False, 'allowed': ['MANCHE NUMERIQUE', 'COLLECTIVITE', 'ORANGE', 'PRIVE', 'AUTRE (à préciser)', 'NUL']}, 
-              'EQ_GEST': {'type': 'string', 'empty': False, 'allowed': ['MANCHE NUMERIQUE', 'MANCHE TELECOM', 'COLLECTIVITE', 'ORANGE', 'MANCHE FIBRE', 'PRIVE', 'AUTRE (à préciser)', 'NUL']}, 
-              'EQ_HAUT': {'empty': False, 'validator': is_float}, 
-              'EQ_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
-              'EQ_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
-              'EQ_STATUT': {'type': 'string', 'empty': False, 'allowed': STATUTS}}
-        
-    def __repr__(self):
-        return "Equipement {}".format(self.EQ_NOM)
-
-class Noeud(BaseGeoModel):
-    filename = "noeud_geo.shp"
-    geom_type = gis_.GEOM_POINT
-    bounding_box = (XMIN,YMIN,XMAX,YMAX)
-    schema = {'NO_CODE': {'type': 'string', 'maxlength': 18}, 
-              'NO_NOM': {'type': 'string', 'maxlength': 30}, 
-              'NO_ID_INSE': {'type': 'string', 'empty': False, 'regex': r'50\d{3}'}, 
-              'NO_VOIE': {'type': 'string', 'maxlength': 100}, 
-              'NO_EMPRISE': {'type': 'string', 'maxlength': 10}, 
-              'NO_ETAT': {'type': 'string', 'empty': False, 'allowed': ['0', '1', '2', '3', '4']}, 
-              'NO_OCCP': {'type': 'string', 'empty': False, 'allowed': ['0', '1.1', '1.2', '2', '3', '4']}, 
-              'NO_TYPE': {'type': 'string', 'empty': False, 'allowed': ['CHA', 'POT', 'LTE', 'SEM', 'FAC', 'OUV', 'IMM']}, 
-              'NO_TYPFCT': {'type': 'string', 'empty': False, 'allowed': ['INTERCONNEXION', 'SATELLITE', 'PASSAGE', 'REGARD', 'INDETERMINE']}, 
-              'NO_TYPE_LQ': {'type': 'string', 'empty': False, 'allowed': ['CHTIR', 'CHRACC', 'POT', 'NRO', 'PM', 'MIMO', 'FAC', 'OUV', 'IMM']}, 
-              'NO_TYPE_PH': {'type': 'string', 'empty': False, 'allowed': ['CHAMBRE', 'POTEAU', 'ARMOIRE', 'SHELTER', 'BATIMENT', 'SITE MIMO', 'FACADE', 'OUVRAGE', 'IMMEUBLE']}, 
-              'NO_CODE_PH': {'type': 'string', 'maxlength': 20}, 
-              'NO_TECH_PS': {'type': 'string', 'maxlength': 20, 'multiallowed': ['COAX', 'CUT', 'ECL', 'ELEC', 'VP', 'OPT', 'NC']}, 
-              'NO_AMO': {'type': 'string', 'maxlength': 20}, 
-              'NO_PLINOX': {'required':False, 'type': 'string', 'maxlength': 3, 'allowed': ['OUI', 'NON']}, 
-              'NO_X': {'empty': False, 'validator': is_float}, 
-              'NO_Y': {'empty': False, 'validator': is_float}, 
-              'NO_PRO': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE', 'COLLECTIVITE', 'ORANGE', 'ERDF', 'PRIVE', 'ENEDIS', 'AUTRE (à préciser)', 'NUL']}, 
-              'NO_GEST': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE', 'MANCHE TELECOM', 'COLLECTIVITE', 'ORANGE', 'ERDF', 'ENEDIS', 'MANCHE FIBRE', 'PRIVE', 'AUTRE (à préciser)', 'NUL']}, 
-              'NO_HAUT': {'empty': False, 'validator': is_float}, 
-              'NO_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
-              'NO_REF_PLA': {'type': 'string', 'maxlength': 100}, 
-              'NO_SRC_GEO': {'type': 'string', 'maxlength': 50}, 
-              'NO_QLT_GEO': {'type': 'string', 'maxlength': 1, 'empty': False, 'allowed': ['A', 'B', 'C']}, 
-              'NO_PRO_MD': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE']}, 
-              'NO_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
-              'NO_STATUT': {'type': 'string', 'empty': False, 'allowed': STATUTS}}
-
-    def __repr__(self):
-        return "Noeud {}".format(self.NO_NOM)
-    
-class Tranchee(BaseGeoModel):
-    filename = "tranchee_geo.shp"
-    geom_type = gis_.GEOM_LINE
-    bounding_box = (XMIN,YMIN,XMAX,YMAX)
-    schema = {'TR_CODE': {'type': 'string', 'maxlength': 23}, 
-              'TR_NOM': {'type': 'string', 'maxlength': 23}, 
-              'TR_ID_INSE': {'type': 'string', 'empty': False, 'regex': r'50\d{3}'}, 
-              'TR_VOIE': {'type': 'string', 'maxlength': 200}, 
-              'TR_TYP_IMP': {'type': 'string', 'empty': False, 'allowed': ['ACCOTEMENT STABILISE', 'ACCOTEMENT NON STABILISE', 'CHAUSSEE LOURDE', 'CHAUSSEE LEGERE', 'FOSSE', 'TROTTOIR', 'ESPACE VERT', 'ENCORBELLEMENT']}, 
-              'TR_MOD_POS': {'type': 'string', 'empty': False, 'allowed': ['TRADITIONNEL', 'MICRO TRANCHEE', 'FONCAGE 60', 'FONCAGE 90', 'FONCAGE 120', 'TRANCHEUSE', 'FORAGE URBAIN', 'FORAGE RURAL', 'ENCORBELLEMENT']}, 
-              'TR_MPOS_AI': {'type': 'string', 'empty': False, 'allowed': ['TRA', 'ALL', 'FONCAGE 60', 'FON', 'FOR', 'ENC']}, 
-              'TR_LONG': {'empty': False, 'validator': is_float}, 
-              'TR_LARG': {'empty': False, 'validator': is_float}, 
-              'TR_REVET': {'empty':True, 'type': 'string', 'allowed': ['SABLE', 'BICOUCHE', 'ENROBE', 'BETON', 'PAVE', 'TERRAIN NATUREL']}, 
-              'TR_CHARGE': {'empty': False, 'validator': is_float}, 
-              'TR_GRILLAG': {'empty':True, 'validator': is_float}, 
-              'TR_REMBLAI': {'type': 'string'}, 
-              'TR_PLYNOX': {'type': 'string', 'empty': False, 'allowed': ['OUI', 'NON']}, 
-              'TR_PRO_VOI': {'type': 'string', 'empty': False, 'allowed': ['COMMUNE', 'COMMUNAUTE DE COMMUNES', 'DEPARTEMENT', 'ETAT', 'PRIVE']}, 
-              'TR_GEST_VO': {'type': 'string', 'empty': False, 'allowed': ['COMMUNE', 'COMMUNAUTE DE COMMUNES', 'DEPARTEMENT', 'ETAT', 'PRIVE']}, 
-              'TR_SCHEMA': {'maxlength': 100, 'type': 'string'}, 
-              'TR_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
-              'TR_SRC_GEO': {'type': 'string', 'maxlength': 50}, 
-              'TR_QLT_GEO': {'type': 'string', 'empty': False, 'allowed': ['A', 'B', 'C']}, 
-              'TR_PRO_MD': {'type': 'string', 'maxlength': 20}, 
-              'TR_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
-              'TR_STATUT': {'type': 'string', 'empty': False, 'allowed': STATUTS}}
-
-    def __repr__(self):
-        return "Tranchee {}".format(self.TR_VOIE)
-    
-class Zapbo(BaseGeoModel):
-    filename = "zapbo_geo.shp"
-    geom_type = gis_.GEOM_POLYGON
-    bounding_box = (XMIN,YMIN,XMAX,YMAX)
-    schema = {'ID_ZAPBO': {'type': 'string', 'maxlength': 30, 'contains_any_of': ['PBO', 'BPE']}, 
-              'COMMENTAIR': {'type': 'string', 'maxlength': 254, 'empty': True}, 
-              'STATUT': {'type': 'string', 'empty': False, 'allowed': STATUTS}}
-    
-    def __repr__(self):
-        return "Zapbo {}".format(self.ID_ZAPBO)

+ 0 - 338
schemas/mn2_rec/validator.py

@@ -1,338 +0,0 @@
-'''
-
-@author: olivier.massot, 2018
-'''
-from core.gis_ import Feature
-from core.validation import NetgeoValidator, RelationError, UniqueError, \
-    InvalidGeometry, DuplicatedGeom, MissingItem, DimensionError, \
-    TechnicalValidationError, PositionError
-from schemas.common import TOLERANCE
-from schemas.mn2_rec.models import Artere, Cable, Equipement, Noeud, \
-    Tranchee, Zapbo
-
-class Mn2RecValidator(NetgeoValidator):
-    schema_name = "MN2 REC"
-    models = [Artere, Cable, Equipement, Noeud, Tranchee, Zapbo]
-    
-    def _technical_validation(self):
-        
-        # construction des index
-        arteres = self.dataset[Artere]
-        cables = self.dataset[Cable]
-        tranchees = self.dataset[Tranchee]
-        
-        noeuds = {}
-        for noeud in self.dataset[Noeud]:
-            if not noeud.NO_NOM in noeuds:
-                noeuds[noeud.NO_NOM] = noeud
-            else:
-                self.log_error(UniqueError("Doublons dans le champs: {}".format(noeud), filename=Noeud.filename, field="NO_NOM"))
-        
-        equipements = {}
-        for equipement in self.dataset[Equipement]:
-            if not equipement.EQ_NOM in equipements:
-                equipements[equipement.EQ_NOM] = equipement
-            else:
-                self.log_error(UniqueError("Doublons dans le champs: {}".format(equipement), filename=Equipement.filename, field="EQ_NOM"))
-                
-        zapbos = {}
-        for zapbo in self.dataset[Zapbo]:
-            if not zapbo.ID_ZAPBO in zapbos:
-                zapbos[zapbo.ID_ZAPBO] = zapbo
-            else:
-                self.log_error(UniqueError("Doublons dans le champs: {}".format(zapbo), filename=Zapbo.filename, field="ID_ZAPBO"))
-        
-        # contrôle de la validité des géométries
-        for artere in arteres:
-            if not artere.valid:
-                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(artere), filename=Artere.filename, field="geom"))
-        for tranchee in tranchees:
-            if not tranchee.valid:
-                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(tranchee), filename=Tranchee.filename, field="geom"))
-        for cable in cables:
-            if not "baguette" in cable.CA_COMMENT.lower() and not cable.valid:
-                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(cable), filename=Cable.filename, field="geom"))
-        for noeud in noeuds.values():
-            if not noeud.valid:
-                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(noeud), filename=Noeud.filename, field="geom"))
-        for equipement in equipements.values():
-            if not equipement.valid:
-                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(equipement), filename=Equipement.filename, field="geom"))
-        for zapbo in zapbos.values():
-            if not zapbo.valid:
-                self.log_error(InvalidGeometry("Géométrie invalide: {}".format(zapbo), filename=Zapbo.filename, field="geom"))
-        
-        # rattachement les noeuds aux artères     
-        for artere in arteres:
-            try:
-                artere.noeud_a = noeuds[artere.AR_NOEUD_A]
-            except KeyError:
-                artere.noeud_a = None
-                self.log_error(RelationError("Le noeud '{}' n'existe pas".format(artere.AR_NOEUD_A), filename=Artere.filename, field="AR_NOEUD_A"))
-                
-            try:
-                artere.noeud_b = noeuds[artere.AR_NOEUD_B]
-            except KeyError:
-                artere.noeud_b = None
-                self.log_error(RelationError("Le noeud '{}' n'existe pas".format(artere.AR_NOEUD_B), filename=Artere.filename, field="AR_NOEUD_A"))
-        
-        # rattachement des equipements aux cables
-        for cable in cables:
-            try:
-                cable.equipement_a = equipements[cable.CA_EQ_A]
-            except KeyError:
-                cable.equipement_a = None
-                self.log_error(RelationError("L'équipement '{}' n'existe pas".format(cable.CA_EQ_A), filename=Cable.filename, field="CA_EQ_A"))
-                
-            try:
-                cable.equipement_b = equipements[cable.CA_EQ_B]
-            except KeyError:
-                cable.equipement_b = None
-                self.log_error(RelationError("L'équipement '{}' n'existe pas".format(cable.CA_EQ_B), filename=Cable.filename, field="CA_EQ_B"))
-
-        # rattachement des equipements aux noeuds
-        for equipement in equipements.values():
-            try:
-                equipement.noeud = noeuds[equipement.EQ_NOM_NOE]
-            except KeyError:
-                equipement.noeud = None
-                self.log_error(RelationError("Le noeud '{}' n'existe pas".format(equipement.EQ_NOM_NOE, equipement.EQ_NOM), filename=Equipement.filename, field="EQ_NOM_NOE"))
-
-        # verifie que tous les equipements sont l'equipement B d'au moins un cable
-        equipements_b = [cable.CA_EQ_B for cable in cables]
-        for eq_id in equipements:
-            if equipements[eq_id].EQ_TYPE == "BAI":
-                continue
-            if not eq_id in equipements_b:
-                self.log_error(RelationError("L'equipement '{}' n'est l'équipement B d'aucun cable".format(eq_id), filename=Equipement.filename, field="EQ_NOM"))
-
-        # controle des doublons graphiques
-        for i, tranchee in enumerate(tranchees):
-            for other in tranchees[i+1:]:
-                if tranchee.geom == other.geom:
-                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée".format(tranchee), filename=Tranchee.filename, field="geom"))
-                    
-        for i, artere in enumerate(arteres):
-            for other in arteres[i+1:]:
-                if artere.geom == other.geom:
-                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('{}')".format(artere), filename=Artere.filename, field="geom"))
-
-        for i, cable in enumerate(cables):
-            for other in cables[i+1:]:
-                if cable.geom == other.geom and cable.CA_EQ_A == other.CA_EQ_A and cable.CA_EQ_B == other.CA_EQ_B:
-                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('{}')".format(cable), filename=Cable.filename, field="geom"))
-        
-        ls_noeuds = list(noeuds.values())
-        for i, noeud in enumerate(ls_noeuds):
-            for other in ls_noeuds[i+1:]:
-                if noeud.geom == other.geom:
-                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('{}')".format(noeud), filename=Noeud.filename, field="geom"))
-        del ls_noeuds
-        
-        ls_zapbos = list(zapbos.values())
-        for i, zapbo in enumerate(ls_zapbos):
-            for other in ls_zapbos[i+1:]:
-                if zapbo.geom == other.geom:
-                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('{}')".format(zapbo), filename=Zapbo.filename, field="geom"))
-        del ls_zapbos
-           
-        # Arteres: comparer la géométrie à celle des noeuds
-        for artere in arteres:
-            if not artere.noeud_a or not artere.noeud_b:
-                continue
-            
-            buffer_a, buffer_b = artere.points[0].Buffer(TOLERANCE), artere.points[-1].Buffer(TOLERANCE)
-            
-            if not (buffer_a.Contains(artere.noeud_a.points[0]) and buffer_b.Contains(artere.noeud_b.points[0])) \
-               and not (buffer_a.Contains(artere.noeud_b.points[0]) and buffer_b.Contains(artere.noeud_a.points[0])):
-
-                self.log_error(MissingItem("Pas de noeud aux coordonnées attendues ('{}')".format(artere), filename=Artere.filename, field="geom"))
-        
-        
-        # Cables: comparer la géométrie à celle des equipements (on utilise en fait la geom du noeud correspondant à l'équipement)
-        for cable in cables:
-            if not cable.equipement_a or not cable.equipement_b or not cable.valid or not cable.equipement_a.noeud or not cable.equipement_b.noeud:
-                continue
-            
-            buffer_a, buffer_b = cable.points[0].Buffer(TOLERANCE), cable.points[-1].Buffer(TOLERANCE)
-            
-            if not (buffer_a.Contains(cable.equipement_a.noeud.points[0]) and buffer_b.Contains(cable.equipement_b.noeud.points[0])) \
-               and not (buffer_a.Contains(cable.equipement_b.noeud.points[0]) and buffer_b.Contains(cable.equipement_a.noeud.points[0])):
-            
-                self.log_error(MissingItem("Pas d'equipement aux coordonnées attendues ('{}')".format(cable), filename=Cable.filename, field="geom"))
-            
-        del buffer_a, buffer_b 
-        
-        # Verifie que chaque tranchée a au moins une artère
-        arteres_emprise = Feature.buffered_union(arteres, TOLERANCE)
-        
-        for tranchee in tranchees:
-            if not arteres_emprise.Contains(tranchee.geom):
-                self.log_error(MissingItem("Tranchée sans artère ('{}')".format(tranchee), filename=Tranchee.filename, field="-"))
-        
-        
-        # Verifie que chaque cable a au moins une artère (sauf si commentaire contient 'baguette')
-        for cable in cables:
-            if "baguette" in cable.CA_COMMENT.lower() or not cable.valid:
-                continue
-            if not arteres_emprise.Contains(cable.geom):
-                self.log_error(MissingItem("Cable sans artère ('{}')".format(cable), filename=Cable.filename, field="-"))
-        
-        del arteres_emprise
-        
-        # Verifie que chaque artère a au moins un cable (sauf si commentaire contient un de ces mots 'racco client adductio attente bus 'sans cable'')
-        cables_emprise = Feature.buffered_union(cables, TOLERANCE)
-        
-        for artere in arteres:
-            if any(x in artere.AR_COMMENT.lower() for x in ['racco','client','adductio','attente','bus','sans cable']):
-                continue
-            if not cables_emprise.Contains(artere.geom):
-                self.log_error(MissingItem("Artère sans cable ('{}')".format(artere), filename=Artere.filename, field="-"))
-                
-        del cables_emprise
-                
-        # Contrôle des dimensions logiques
-        for artere in arteres:
-            try:
-                if not int(artere.AR_FOU_DIS) <= int(artere.AR_NB_FOUR):
-                    self.log_error(DimensionError("Le nombre de fourreaux disponibles doit être inférieur au nombre total ('{}')".format(artere), filename=Artere.filename, field="AR_FOU_DIS"))
-            except (TypeError, ValueError):
-                pass
-        
-        for cable in cables:
-            try:
-                if not int(cable.CA_NB_FO_U) <= int(cable.CA_NB_FO):
-                    self.log_error(DimensionError("Le nombre de fourreaux utilisés doit être inférieur au nombre total ('{}')".format(cable), filename=Cable.filename, field="CA_NB_FO_U"))
-                if not int(cable.CA_NB_FO_D) <= int(cable.CA_NB_FO):
-                    self.log_error(DimensionError("Le nombre de fourreaux disponibles doit être inférieur au nombre total ('{}')".format(cable), filename=Cable.filename, field="CA_NB_FO_D"))
-            except (TypeError, ValueError):
-                pass
-        
-        ant_db = mn.ANTDb_0()
-        ant_db.execute("alter session set NLS_NUMERIC_CHARACTERS = '.,';") # definit le separateur decimal sur '.'
-        
-        # Toutes les zapbo contiennent au moins une prise
-        for zapbo in zapbos.values():
-            
-            if len(zapbo.points) >= 499:
-                # passe l'erreur provoquée par la limite au nombre d'arguments en SQL
-                zapbo.nb_prises = None
-                continue
-            
-            sql = """Select SUM(NB_PRISE) AS NB_PRISES FROM SIG_ANT.FTTH_MN_PRISE_LOT z 
-                       WHERE SDO_INSIDE(z.GEOMETRY,
-                                      SDO_GEOMETRY(2003, 3949, SDO_POINT_TYPE(null,null,null), SDO_ELEM_INFO_ARRAY(1,1003,1),  SDO_ORDINATE_ARRAY({}))
-                                         )='TRUE';""".format(", ".join(["{},{}".format(p.GetX(), p.GetY()) for p in zapbo.points]))
-            
-            zapbo.nb_prises = int(ant_db.first(sql).NB_PRISES)
-            if not zapbo.nb_prises:
-                self.log_error(MissingItem("La Zapbo ne contient aucune prise: {}".format(zapbo), filename=Zapbo.filename, field="-"))
-        
-        # Toutes les prises de la ou les ZAPM impactées sont dans une zapbo
-        zapms = {}
-        # > on déduit la liste des zapms à partir de la position des zapbos
-        for zapbo in zapbos.values():
-            centre = zapbo.geom.Centroid()
-            zapm = ant_db.first("""SELECT z.ID_ZAPM 
-                                  FROM SIG_ANT.FTTH_MN_ZAPM z
-                                  WHERE sdo_contains(z.GEOMETRY, 
-                                                     SDO_GEOMETRY(2001, 3949, SDO_POINT_TYPE({}, {}, NULL), NULL, NULL)) = 'TRUE'
-                               """.format(centre.GetX(), centre.GetY()))
-            try:
-                zapms[zapm.ID_ZAPM].append(zapbo)
-            except KeyError:
-                zapms[zapm.ID_ZAPM] = [zapbo]
-        
-        for id_zapm in zapms:
-            zapm_couverture = Feature.union(zapms[id_zapm])
-            for prise in ant_db.read("""SELECT t.X AS x, t.Y AS y
-                                        FROM SIG_ANT.FTTH_MN_PRISE_LOT z, 
-                                        TABLE(SDO_UTIL.GETVERTICES(z.GEOMETRY)) t 
-                                        WHERE T_ETAT<>'OBSOLETE' AND ID_ZAPM_PARTIELLE='{}';""".format(id_zapm)):
-                point = ogr.Geometry(ogr.wkbPoint)
-                point.AddPoint(prise.x, prise.y)
-                if not zapm_couverture.Contains(point):
-                    self.log_error(MissingItem("Certaines prises de la ZAPM ne sont pas comprises dans une ZAPBO: {}".format(id_zapm), filename=Zapbo.filename, field="-"))
-        
-        # Verifier que chaque equipement de type PBO est contenu dans une zapbo, et que le nom de la zapbo contient le nom de l'equipement
-        
-        for equipement in equipements.values():
-            if not equipement.EQ_TYPE == "PBO":
-                continue
-            
-            #zapbos englobant l'equipement
-            candidates = []
-            for zapbo in zapbos.values():
-                if zapbo.geom.Contains(equipement.geom):
-                    candidates.append(zapbo)
-                    
-            # le pbo doit être contenu dans une zapbo
-            if not candidates:
-                self.log_error(MissingItem("Le PBO n'est contenu dans aucune ZAPBO: {}".format(equipement), filename=Equipement.filename, field="geom"))
-                continue
-            
-            # On se base sur le nom pour trouver la zapbo correspondante
-            try:
-                equipement.zapbo = next((z for z in candidates if equipement.EQ_NOM in z.ID_ZAPBO))
-            except StopIteration:
-                self.log_error(MissingItem("Le nom du PBO ne coincide avec le nom d'aucune des ZAPBO qui le contient: {}".format(equipement), filename=Equipement.filename, field="EQ_NOM"))
-                break
-            
-            if equipement.zapbo.nb_prises is None:
-                equipement.zapbo.nb_prises = 0
-            
-            # Controle du dimensionnement des PBO
-            if equipement.zapbo.nb_prises is not None:
-                if equipement.EQ_TYPE_PH == 'PBO 6' and not equipement.zapbo.nb_prises < 6:
-                    self.log_error(DimensionError("Le PBO 6 contient plus de 5 prises: {}".format(equipement), filename=Equipement.filename, field="-"))
-            
-                if equipement.EQ_TYPE_PH == 'PBO 12' and not equipement.zapbo.nb_prises >= 6 and equipement.zapbo.nb_prises <= 8:
-                    self.log_error(DimensionError("Le PBO 12 contient mois de 6 prises ou plus de 8 prises: {}".format(equipement), filename=Equipement.filename, field="-"))
-        
-            if equipement.zapbo.STATUT == "REC" and not equipement.EQ_STATUT == "REC":
-                self.log_error(TechnicalValidationError("Le statut du PBO n'est pas cohérent avec le statut de sa ZAPBO: {}".format(equipement), filename=Equipement.filename, field="-"))
-        
-            if equipement.EQ_STATUT == "REC" and not equipement.zapbo.STATUT == "REC" and not equipement.zapbo.ID_ZAPBO[:4].lower() == "att_":
-                self.log_error(TechnicalValidationError("Le statut du PBO n'est pas cohérent avec le statut de sa ZAPBO: {}".format(equipement), filename=Equipement.filename, field="-"))
-        
-        # Contrôler dans la base si des éléments portant ces codes existent à des emplacements différents
-        for noeud in noeuds.values():
-            sql = """SELECT z.NO_NOM, SDO_GEOM.SDO_DISTANCE(z.GEOMETRY, SDO_GEOMETRY(2001, 3949, SDO_POINT_TYPE({}, {}, NULL), NULL, NULL),0.005) AS DIST 
-                     FROM SIG_ANT.FTTH_MN_GR_NOEUD_GEO z 
-                     WHERE z.NO_NOM='{}';""".format(noeud.geom.GetX(), noeud.geom.GetY(), noeud.NO_NOM)
-            existing = ant_db.first(sql)
-            if existing:
-                if existing.DIST > TOLERANCE and existing.DIST < 20:
-                    self.log_error(PositionError("La position du noeud ne correspond pas à l'existant: {}".format(noeud), filename=Noeud.filename, field="geom"))
-                elif existing.DIST > 20:
-                    self.log_error(DuplicatedGeom("Un noeud portant ce nom existe déjà ailleurs sur le territoire: {}".format(noeud), filename=Noeud.filename, field="NO_NOM"))
-        
-        for zapbo in zapbos.values():
-            sql = """SELECT z.ID_ZAPBO, SDO_GEOM.SDO_DISTANCE(SDO_GEOM.SDO_CENTROID(z.GEOMETRY,0.005), SDO_GEOMETRY(2001, 3949, SDO_POINT_TYPE({}, {}, NULL), NULL, NULL),0.005) AS DIST 
-                     FROM SIG_ANT.FTTH_MN_GR_ZAPBO_GEO z 
-                     WHERE z.ID_ZAPBO='{}';""".format(zapbo.geom.Centroid().GetX(), zapbo.geom.Centroid().GetY(), zapbo.ID_ZAPBO)
-            existing = ant_db.first(sql)
-            if existing:
-                if existing.DIST > TOLERANCE and existing.DIST < 20:
-                    self.log_error(PositionError("La position de la ZAPBO ne correspond pas à l'existant: {}".format(zapbo), filename=Zapbo.filename, field="geom"))
-                elif existing.DIST > 20:
-                    self.log_error(DuplicatedGeom("Une ZAPBO portant ce nom existe déjà ailleurs sur le territoire: {}".format(zapbo), filename=Zapbo.filename, field="ID_ZAPBO"))
-
-        # Contrôle si un equipement portant ce nom existe, mais associé à un noeud différent
-        for equipement in equipements.values():
-            sql = """SELECT z.EQ_NOM, z.EQ_NOM_NOEUD
-                     FROM SIG_ANT.FTTH_MN_GR_EQ_PASSIF z 
-                     WHERE z.EQ_NOM='{}';""".format(equipement.EQ_NOM)
-            existing = ant_db.first(sql)
-            if existing and existing.EQ_NOM_NOEUD != equipement.EQ_NOM_NOE:
-                self.log_error(DuplicatedGeom("Un équipement portant ce nom ({}) existe déjà et est associé à un noeud différent ({})".format(equipement.NO_NOM, existing.EQ_NOM_NOEUD), filename=Noeud.filename, field="geom"))
-        
-        
-        
-    
-if __name__ == "__main__":
-    from core.constants import MAIN
-    subject = MAIN / "work" / "STURNO_228CP0_APD_180301_OK"
-    report = Netgeo22DoeValidator.submit(subject)
-    print(report)
-    

+ 0 - 0
schemas/mn3_exe/__init__.py


+ 0 - 0
schemas/mn3_pro/__init__.py


+ 0 - 0
schemas/mn3_rec/__init__.py


+ 68 - 0
ui/checkframe.ui

@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Frame</class>
+ <widget class="QFrame" name="Frame">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>393</width>
+    <height>29</height>
+   </rect>
+  </property>
+  <property name="font">
+   <font>
+    <family>Verdana</family>
+    <pointsize>9</pointsize>
+   </font>
+  </property>
+  <property name="windowTitle">
+   <string>Frame</string>
+  </property>
+  <property name="frameShape">
+   <enum>QFrame::StyledPanel</enum>
+  </property>
+  <property name="frameShadow">
+   <enum>QFrame::Raised</enum>
+  </property>
+  <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1">
+   <property name="spacing">
+    <number>12</number>
+   </property>
+   <property name="leftMargin">
+    <number>12</number>
+   </property>
+   <property name="topMargin">
+    <number>3</number>
+   </property>
+   <property name="rightMargin">
+    <number>3</number>
+   </property>
+   <property name="bottomMargin">
+    <number>3</number>
+   </property>
+   <item>
+    <widget class="QLabel" name="lbl_icon">
+     <property name="text">
+      <string/>
+     </property>
+     <property name="pixmap">
+      <pixmap>rsc/circles-loader.png</pixmap>
+     </property>
+     <property name="scaledContents">
+      <bool>false</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="lbl_message">
+     <property name="text">
+      <string>...</string>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 73 - 67
ui/dlg_main.py

@@ -1,113 +1,119 @@
-# -*- coding: utf-8 -*-
 """
-/***************************************************************************
- MnCheckDialog
-                                 A QGIS plugin
- Contrôle des données FTTH format MN
- Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
-                             -------------------
-        begin                : 2018-12-07
-        git sha              : $Format:%H$
-        copyright            : (C) 2018 by Manche Numérique 2019
-        email                : olivier.massot@manchenumerique.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.                                   *
- *                                                                         *
- ***************************************************************************/
 """
 import importlib
 import logging
 
+from qgis.core import QgsProject #@UnresolvedImport
+
 from PyQt5 import QtWidgets
 from PyQt5 import uic
-from PyQt5.Qt import QIcon, QPixmap
+from PyQt5.Qt import QIcon, QPixmap, Qt, QTableWidgetItem, QHeaderView
 
 from core.constants import MAIN, RSCDIR
 
+
+
 logger = logging.getLogger("mncheck")
 
 Ui_Main, _ = uic.loadUiType(MAIN / 'ui'/ 'dlg_main.ui')
+Ui_ChkFrame, _ = uic.loadUiType(MAIN / 'ui'/ 'checkframe.ui')
 
-SCHEMAS = ["mn1_rec", "mn2_rec", "mn3_pro", "mn3_exe", "mn3_rec"]
 
+SCHEMAS = ["mn1_rec", "mn2_rec", "mn3_pro", "mn3_exe", "mn3_rec"]
 
 
 class DlgMain(QtWidgets.QDialog):
-    def __init__(self, parent=None):
+    def __init__(self, iface, parent=None):
         super(DlgMain, self).__init__(parent)
         self.createWidgets()
+        self.iface = iface
+        self.schema_lib = None
 
     def createWidgets(self):
         """ set up the interface """
         self.ui = Ui_Main()
         self.ui.setupUi(self)
         
-        self.ui.stack.setCurrentIndex(0)
-        
-        self.ui.btn_play.setIcon(QIcon(RSCDIR / "play.png"))
-        self.ui.btn_play.clicked.connect(self.run)
+        self.ui.btn_run.setIcon(QIcon(RSCDIR / "play.png"))
+        self.ui.btn_run.clicked.connect(self.run)
         
         self.ui.btn_help.setIcon(QIcon(RSCDIR / "question.png"))
         self.ui.btn_help.clicked.connect(self.show_help)
         
         self.ui.btn_settings.setIcon(QIcon(RSCDIR / "settings.png"))
         self.ui.btn_settings.clicked.connect(self.show_settings)
-        
-        self.ui.lbl_hourglass.setPixmap(QPixmap(RSCDIR / "hourglass.png"))
-        self.ui.lbl_ok.setPixmap(QPixmap(RSCDIR / "ok_32.png"))
-
-        self.ui.cbb_schemas.addItem("Schéma MN v1", 0)
-        self.ui.cbb_schemas.addItem("Schéma MN v2", 1)
-        self.ui.cbb_schemas.addItem("Schéma MN v3 PRO", 2)
-        self.ui.cbb_schemas.addItem("Schéma MN v3 EXE", 3)
-        self.ui.cbb_schemas.addItem("Schéma MN v3 REC", 4)
-#         self.ui.cbb_schemas.addItem("GraceTHD v2.1", 5)
+
+        self.ui.tbl_layers.horizontalHeaderItem(1).setTextAlignment(Qt.AlignLeft)
+
+        self.ui.cbb_schemas.addItem("Schéma MN v1 REC", 0)
+        self.ui.cbb_schemas.addItem("Schéma MN v2 REC", 1)
+#         self.ui.cbb_schemas.addItem("Schéma MN v3 PRO", 2)
+#         self.ui.cbb_schemas.addItem("Schéma MN v3 EXE", 3)
+#         self.ui.cbb_schemas.addItem("Schéma MN v3 REC", 4)
         self.ui.cbb_schemas.currentIndexChanged.connect(self.update_layers_list)
         
         self.update_layers_list()
     
+    def clear_layers_table(self):
+        while self.ui.tbl_layers.rowCount():
+            self.ui.tbl_layers.removeRow(0)
     
-    def update_layers_list(self):
-        
+    def selected_validator(self):
         schema_lib_name = SCHEMAS[int(self.ui.cbb_schemas.itemData(self.ui.cbb_schemas.currentIndex()))]
-        
+        logger.info("Selected schema: %s", schema_lib_name)
+    
         schema_lib = importlib.import_module("schemas." + schema_lib_name)
+        return schema_lib.Validator
+    
+    def update_layers_list(self):
+        
+        validator = self.selected_validator()
+
+        expected = [model.layername for model in validator.models]
+        logger.info("Expected layers: %s", str(expected))
+
+        found = [layer.name().lower() for layer in QgsProject.instance().mapLayers().values()]
+        logger.info("Found layers: %s", str(found))
+
+        self.ui.btn_run.setEnabled(True)
+
+        self.clear_layers_table()
+
+        for layername in expected:
+            
+            cur_row = self.ui.tbl_layers.rowCount()
+            self.ui.tbl_layers.insertRow(cur_row)
+            
+            lbl_icon, lbl_layer, lbl_message = QTableWidgetItem(), QTableWidgetItem(), QTableWidgetItem()
 
-        logger.info("Selection du schema '{%s}'", schema_lib)
+            lbl_layer.setText(layername)
+
+            if layername.lower() in found:
+                lbl_icon.setIcon(QIcon(QPixmap(RSCDIR / "ok_16.png")))
+            else:
+                lbl_icon.setIcon(QIcon(QPixmap(RSCDIR / "error_16.png")))
+                lbl_message.setText("La couche est introuvable")
+                self.ui.btn_run.setEnabled(False)
+                
+            self.ui.tbl_layers.setItem(cur_row, 0, lbl_icon)
+            self.ui.tbl_layers.setItem(cur_row, 1, lbl_layer)
+            self.ui.tbl_layers.setItem(cur_row, 2, lbl_message)
+        
+        self.ui.tbl_layers.horizontalHeader().setSectionResizeMode(0, QHeaderView.Fixed)
+        self.ui.tbl_layers.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents)
+        self.ui.tbl_layers.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch)
 
     def run(self):
-        pass
         
-#         try:
-#             f = request.FILES['dossier']
-#         except KeyError:
-#             return render(request, "index.html", {"validation_error": "Aucun fichier sélectionné"})
-#         
-#         filename = secure_filename(f.name)
-#         
-#         if Path(filename).ext != ".zip":
-#             return render(request, "index.html", {"validation_error": "Le fichier doit être un fichier .ZIP ({})".format(Path(filename).ext)})
-#         
-#         schema_lib_name = request.POST['schema']
-#         schema_lib = importlib.import_module("schemas." + schema_lib_name)
-#         
-#     #     try:
-#         with TemporaryDirectory(dir=MAIN / "upload") as d:
-#             filename = Path(d) / filename
-#             with open(filename, 'wb+') as destination:
-#                 for chunk in f.chunks():
-#                     destination.write(chunk)
-#             report = schema_lib.validator.submit(filename)
-#     #     except Exception as e:
-#     #         return render_template("index.html", validation_error=str(e))
-#     
-#     return render(request, "report.html", {"report": report})
+        validator = self.selected_validator()
+        
+        if not validator:
+            logger.info("Aucun schéma sélectionné - Opération annulée")
+            return
+        
+        report = validator.submit(self.iface)
+        
+        logger.info(report)
     
     def show_help(self):
         pass

+ 166 - 273
ui/dlg_main.ui

@@ -6,8 +6,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>587</width>
-    <height>445</height>
+    <width>490</width>
+    <height>448</height>
    </rect>
   </property>
   <property name="font">
@@ -23,280 +23,173 @@
    <iconset>
     <normaloff>rsc/network.png</normaloff>rsc/network.png</iconset>
   </property>
-  <layout class="QVBoxLayout" name="verticalLayout_3">
+  <layout class="QVBoxLayout" name="verticalLayout">
    <item>
-    <widget class="QStackedWidget" name="stack">
-     <property name="currentIndex">
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <property name="topMargin">
       <number>0</number>
      </property>
-     <widget class="QWidget" name="page_1">
-      <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,1,0,0">
-       <item>
-        <layout class="QHBoxLayout" name="horizontalLayout_2">
-         <property name="leftMargin">
-          <number>9</number>
-         </property>
-         <property name="rightMargin">
-          <number>9</number>
-         </property>
-         <item>
-          <widget class="QComboBox" name="cbb_schemas">
-           <property name="minimumSize">
-            <size>
-             <width>0</width>
-             <height>30</height>
-            </size>
-           </property>
-          </widget>
-         </item>
-         <item>
-          <widget class="QToolButton" name="btn_play">
-           <property name="minimumSize">
-            <size>
-             <width>32</width>
-             <height>32</height>
-            </size>
-           </property>
-           <property name="text">
-            <string/>
-           </property>
-           <property name="icon">
-            <iconset>
-             <normaloff>rsc/play.png</normaloff>rsc/play.png</iconset>
-           </property>
-          </widget>
-         </item>
-        </layout>
-       </item>
-       <item>
-        <layout class="QVBoxLayout" name="layers_layout"/>
-       </item>
-       <item>
-        <spacer name="verticalSpacer_3">
-         <property name="orientation">
-          <enum>Qt::Vertical</enum>
-         </property>
-         <property name="sizeHint" stdset="0">
-          <size>
-           <width>20</width>
-           <height>40</height>
-          </size>
-         </property>
-        </spacer>
-       </item>
-       <item>
-        <layout class="QHBoxLayout" name="horizontalLayout">
-         <property name="topMargin">
-          <number>0</number>
-         </property>
-         <item>
-          <spacer name="horizontalSpacer">
-           <property name="orientation">
-            <enum>Qt::Horizontal</enum>
-           </property>
-           <property name="sizeHint" stdset="0">
-            <size>
-             <width>40</width>
-             <height>20</height>
-            </size>
-           </property>
-          </spacer>
-         </item>
-         <item>
-          <widget class="QPushButton" name="btn_help">
-           <property name="enabled">
-            <bool>false</bool>
-           </property>
-           <property name="minimumSize">
-            <size>
-             <width>100</width>
-             <height>28</height>
-            </size>
-           </property>
-           <property name="font">
-            <font>
-             <pointsize>9</pointsize>
-            </font>
-           </property>
-           <property name="text">
-            <string>Aide</string>
-           </property>
-           <property name="icon">
-            <iconset>
-             <normaloff>rsc/question.png</normaloff>rsc/question.png</iconset>
-           </property>
-          </widget>
-         </item>
-         <item>
-          <widget class="QPushButton" name="btn_settings">
-           <property name="enabled">
-            <bool>false</bool>
-           </property>
-           <property name="minimumSize">
-            <size>
-             <width>100</width>
-             <height>28</height>
-            </size>
-           </property>
-           <property name="font">
-            <font>
-             <pointsize>9</pointsize>
-            </font>
-           </property>
-           <property name="text">
-            <string>Options</string>
-           </property>
-           <property name="icon">
-            <iconset>
-             <normaloff>rsc/settings.png</normaloff>rsc/settings.png</iconset>
-           </property>
-          </widget>
-         </item>
-        </layout>
-       </item>
-      </layout>
-     </widget>
-     <widget class="QWidget" name="page_2">
-      <layout class="QVBoxLayout" name="verticalLayout">
-       <item>
-        <widget class="QLabel" name="lbl_hourglass">
-         <property name="text">
-          <string/>
-         </property>
-         <property name="pixmap">
-          <pixmap>rsc/hourglass.png</pixmap>
-         </property>
-         <property name="alignment">
-          <set>Qt::AlignCenter</set>
-         </property>
-        </widget>
-       </item>
-      </layout>
-     </widget>
-     <widget class="QWidget" name="page_3">
-      <layout class="QVBoxLayout" name="verticalLayout_4">
-       <item>
-        <widget class="QTableWidget" name="tbl_errors">
-         <attribute name="horizontalHeaderStretchLastSection">
-          <bool>true</bool>
-         </attribute>
-         <column>
-          <property name="text">
-           <string>type_err</string>
-          </property>
-         </column>
-         <column>
-          <property name="text">
-           <string>layer_name</string>
-          </property>
-         </column>
-         <column>
-          <property name="text">
-           <string>id_item</string>
-          </property>
-         </column>
-         <column>
-          <property name="text">
-           <string>description</string>
-          </property>
-         </column>
-        </widget>
-       </item>
-      </layout>
-     </widget>
-     <widget class="QWidget" name="page_4">
-      <layout class="QVBoxLayout" name="verticalLayout_5">
-       <item>
-        <spacer name="verticalSpacer_2">
-         <property name="orientation">
-          <enum>Qt::Vertical</enum>
-         </property>
-         <property name="sizeHint" stdset="0">
-          <size>
-           <width>20</width>
-           <height>40</height>
-          </size>
-         </property>
-        </spacer>
-       </item>
-       <item>
-        <layout class="QHBoxLayout" name="horizontalLayout_3" stretch="1,0,0,1">
-         <property name="spacing">
-          <number>24</number>
-         </property>
-         <item>
-          <spacer name="horizontalSpacer_2">
-           <property name="orientation">
-            <enum>Qt::Horizontal</enum>
-           </property>
-           <property name="sizeHint" stdset="0">
-            <size>
-             <width>40</width>
-             <height>20</height>
-            </size>
-           </property>
-          </spacer>
-         </item>
-         <item>
-          <widget class="QLabel" name="lbl_ok">
-           <property name="text">
-            <string/>
-           </property>
-           <property name="pixmap">
-            <pixmap>rsc/ok_32.png</pixmap>
-           </property>
-          </widget>
-         </item>
-         <item>
-          <widget class="QLabel" name="label">
-           <property name="text">
-            <string>Aucune erreur détectée</string>
-           </property>
-          </widget>
-         </item>
-         <item>
-          <spacer name="horizontalSpacer_3">
-           <property name="orientation">
-            <enum>Qt::Horizontal</enum>
-           </property>
-           <property name="sizeHint" stdset="0">
-            <size>
-             <width>40</width>
-             <height>20</height>
-            </size>
-           </property>
-          </spacer>
-         </item>
-        </layout>
-       </item>
-       <item>
-        <spacer name="verticalSpacer">
-         <property name="orientation">
-          <enum>Qt::Vertical</enum>
-         </property>
-         <property name="sizeHint" stdset="0">
-          <size>
-           <width>20</width>
-           <height>40</height>
-          </size>
-         </property>
-        </spacer>
-       </item>
-       <item>
-        <widget class="QPushButton" name="btn_zipfiles">
-         <property name="minimumSize">
-          <size>
-           <width>0</width>
-           <height>36</height>
-          </size>
-         </property>
-         <property name="text">
-          <string>Générer le livrable</string>
-         </property>
-        </widget>
-       </item>
-      </layout>
-     </widget>
+     <item>
+      <widget class="QLabel" name="label">
+       <property name="text">
+        <string>Sélectionnez le schéma à appliquer:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="horizontalSpacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QPushButton" name="btn_help">
+       <property name="enabled">
+        <bool>false</bool>
+       </property>
+       <property name="minimumSize">
+        <size>
+         <width>28</width>
+         <height>28</height>
+        </size>
+       </property>
+       <property name="font">
+        <font>
+         <pointsize>9</pointsize>
+        </font>
+       </property>
+       <property name="text">
+        <string/>
+       </property>
+       <property name="icon">
+        <iconset>
+         <normaloff>rsc/question.png</normaloff>rsc/question.png</iconset>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="btn_settings">
+       <property name="enabled">
+        <bool>false</bool>
+       </property>
+       <property name="minimumSize">
+        <size>
+         <width>28</width>
+         <height>28</height>
+        </size>
+       </property>
+       <property name="font">
+        <font>
+         <pointsize>9</pointsize>
+        </font>
+       </property>
+       <property name="text">
+        <string/>
+       </property>
+       <property name="icon">
+        <iconset>
+         <normaloff>rsc/settings.png</normaloff>rsc/settings.png</iconset>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_2">
+     <property name="leftMargin">
+      <number>0</number>
+     </property>
+     <property name="rightMargin">
+      <number>0</number>
+     </property>
+     <item>
+      <widget class="QComboBox" name="cbb_schemas">
+       <property name="minimumSize">
+        <size>
+         <width>0</width>
+         <height>30</height>
+        </size>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QToolButton" name="btn_run">
+       <property name="minimumSize">
+        <size>
+         <width>32</width>
+         <height>32</height>
+        </size>
+       </property>
+       <property name="text">
+        <string/>
+       </property>
+       <property name="icon">
+        <iconset>
+         <normaloff>rsc/play.png</normaloff>rsc/play.png</iconset>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QTableWidget" name="tbl_layers">
+     <property name="editTriggers">
+      <set>QAbstractItemView::NoEditTriggers</set>
+     </property>
+     <property name="dragDropOverwriteMode">
+      <bool>false</bool>
+     </property>
+     <property name="selectionMode">
+      <enum>QAbstractItemView::NoSelection</enum>
+     </property>
+     <property name="cornerButtonEnabled">
+      <bool>false</bool>
+     </property>
+     <attribute name="horizontalHeaderVisible">
+      <bool>false</bool>
+     </attribute>
+     <attribute name="horizontalHeaderDefaultSectionSize">
+      <number>30</number>
+     </attribute>
+     <attribute name="horizontalHeaderMinimumSectionSize">
+      <number>30</number>
+     </attribute>
+     <attribute name="horizontalHeaderStretchLastSection">
+      <bool>true</bool>
+     </attribute>
+     <attribute name="verticalHeaderVisible">
+      <bool>false</bool>
+     </attribute>
+     <column>
+      <property name="text">
+       <string>icon</string>
+      </property>
+      <property name="textAlignment">
+       <set>AlignHCenter|AlignVCenter|AlignCenter</set>
+      </property>
+     </column>
+     <column>
+      <property name="text">
+       <string>layer</string>
+      </property>
+      <property name="font">
+       <font>
+        <family>Verdana</family>
+       </font>
+      </property>
+     </column>
+     <column>
+      <property name="text">
+       <string>message</string>
+      </property>
+     </column>
     </widget>
    </item>
   </layout>

BIN
ui/rsc/actu.png


BIN
ui/rsc/circles-loader.png


BIN
ui/rsc/settings-work-tool.png