mn3_apd.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. '''
  2. Schéma de validation des données MN2
  3. @author: olivier.massot, 2018
  4. '''
  5. import logging
  6. from qgis.core import QgsProject
  7. from core.cerberus_ import is_positive_int, is_positive_float, CerberusValidator, \
  8. CerberusErrorHandler, _translate_messages
  9. from core.checking import BaseChecker
  10. from core.mncheck import QgsModel
  11. logger = logging.getLogger("mncheck")
  12. SCHEMA_NAME = "Schéma MN v3 APD"
  13. XMIN, XMAX, YMIN, YMAX = 1341999, 1429750, 8147750, 8294000
  14. CRS = 'EPSG:3949' # Coordinate Reference System
  15. TOLERANCE = 0.5
  16. STATUTS = ['ETUDE PRELIMINAIRE', 'ETUDE DE DIAGNOSTIC', 'AVANT-PROJET PROJET', 'PROJET', 'PASSATION DES MARCHES DE TRAVAUX',
  17. 'ETUDE D EXECUTION', 'TRAVAUX' , 'RECOLEMENT', 'MAINTIEN EN CONDITIONS OPERATIONNELLES']
  18. ##### Modeles
  19. class SiteTelecom(QgsModel):
  20. layername = "SITE_TELECOM"
  21. geom_type = QgsModel.GEOM_POINT
  22. bounding_box = (XMIN,YMIN,XMAX,YMAX)
  23. pk = "ST_CODE"
  24. schema = {'ST_CODE': {'type': 'string', 'empty': False},
  25. 'ST_NOM': {'type': 'string', 'empty': False},
  26. 'ST_TYPFCT': {'type': 'string', 'empty': False, 'allowed': ['SITE HEBERGEMENT',
  27. 'NOEUD RACCORDEMENT OPTIQUE',
  28. 'SOUS-REPARTITEUR OPTIQUE',
  29. 'SOUS-REPARTITEUR OPTIQUE COLOCALISE',
  30. 'NOEUD RACCORDEMENT D ABONNES',
  31. 'NOEUD RACCORDEMENT D ABONNES - HAUT DEBIT',
  32. 'NOEUD RACCORDEMENT D ABONNES - MONTEE EN DEBIT']},
  33. 'ST_STATUT': {'type': 'string', 'empty': False, 'allowed': [STATUTS]},
  34. 'ST_NBPRISE': {'empty': False, 'validator': is_positive_int}
  35. }
  36. class SiteClient(QgsModel):
  37. layername = "SITE_CLIENT"
  38. geom_type = QgsModel.GEOM_POINT
  39. bounding_box = (XMIN,YMIN,XMAX,YMAX)
  40. schema = {'SC_TYPFON': {'type': 'string', 'empty': False, 'allowed': ['RESIDENTIEL', 'PROFESSIONNEL', 'OPERATEUR', 'TECHNIQUE']},
  41. 'SC_STATUT': {'type': 'string', 'empty': False, 'allowed': [STATUTS]},
  42. 'SC_NBPRISE': {'empty': False, 'validator': is_positive_int},
  43. 'SC_NBPRHAB': {'empty': False, 'validator': is_positive_int},
  44. 'SC_NBPRPRO': {'empty': False, 'validator': is_positive_int},
  45. 'SC_ID_SITE': {'type': 'string', 'empty': False},
  46. 'SC_NUM_RUE': {'type': 'string', 'empty': False},
  47. 'SC_REPET': {'type': 'string', 'empty': False},
  48. 'SC_DNUBAT': {'type': 'string', 'empty': False},
  49. 'SC_DESC': {'type': 'string', 'empty': False},
  50. 'SC_NOM_RUE': {'type': 'string', 'empty': False},
  51. 'SC_NOM_COM': {'type': 'string', 'empty': False},
  52. 'SC_ETAT': {'type': 'string', 'empty': False, 'allowed': ['VALIDE', 'OBSOLETE', 'NOUVEAU', 'FUTUR']},
  53. 'SC_ATT_PTO': {'empty': False, 'validator': is_positive_float}
  54. }
  55. class Cable(QgsModel):
  56. layername = "CABLE"
  57. geom_type = QgsModel.GEOM_LINE
  58. bounding_box = (XMIN,YMIN,XMAX,YMAX)
  59. schema = {'CA_TYPFON': {'type': 'string', 'empty': False, 'allowed': ['COLLECTE TRANSPORT DISTRIBUTION', 'COLLECTE', 'COLLECTE TRANSPORT', 'COLLECTE DISTRIBUTION', 'TRANSPORT DISTRIBUTION', 'TRANSPORT', 'DISTRIBUTION', 'RACCORDEMENT FINAL', 'BOUCLE METROPOLITAINE', 'LONGUE DISTANCE (LONG HAUL)', 'NON COMMUNIQUE']},
  60. 'CA_TYPSTR': {'type': 'string', 'empty': False, 'allowed': ['CONDUITE', 'AERIEN', 'COLONNE MONTANTE', 'IMMERGE', 'FACADE']},
  61. 'CA_STATUT': {'type': 'string', 'empty': False, 'allowed': [STATUTS]},
  62. 'CA_CAPFO': {'empty': False, 'allowed': [720,576,288,144,96,72,48,24,12]},
  63. 'CA_MODULO': {'empty': False, 'validator': is_positive_int},
  64. 'CA_SUPPORT': {'type': 'string', 'empty': False, 'allowed': ['0', '1']}
  65. }
  66. class Zapbo(QgsModel):
  67. layername = "ZAPBO"
  68. geom_type = QgsModel.GEOM_POLYGON
  69. bounding_box = (XMIN,YMIN,XMAX,YMAX)
  70. schema = {'ZP_NBPRISE': {'empty': False, 'validator': is_positive_int},
  71. 'ZP_ISOLE': {'type': 'string', 'empty': False, 'allowed': ['0', '1']},
  72. 'ZP_STATUT': {'type': 'string', 'empty': False, 'allowed': [STATUTS]}
  73. }
  74. class Zasro(QgsModel):
  75. layername = "ZASRO"
  76. geom_type = QgsModel.GEOM_POLYGON
  77. bounding_box = (XMIN,YMIN,XMAX,YMAX)
  78. schema = {'ZS_CODE': {'type': 'string', 'empty': False},
  79. 'ZS_NBPRISE': {'empty': False, 'validator': is_positive_int}
  80. }
  81. class Adduction(QgsModel):
  82. layername = "ADDUCTION"
  83. geom_type = QgsModel.GEOM_LINE
  84. bounding_box = (XMIN,YMIN,XMAX,YMAX)
  85. schema = {'AD_ISOLE': {'type': 'string', 'empty': False, 'allowed': ['0', '1']},
  86. 'AD_LONG': {'empty': False, 'validator': is_positive_float}
  87. }
  88. models = [SiteTelecom, SiteClient, Cable, Zapbo, Zasro, Adduction]
  89. ####### Validateur
  90. class Mn3ApdChecker(BaseChecker):
  91. def setUp(self):
  92. self.dataset = {}
  93. for model in models:
  94. model.layer = next((l for l in QgsProject.instance().mapLayers().values() \
  95. if l.name().lower() == model.layername.lower()), None)
  96. self.dataset[model] = [model(f) for f in model.layer.getFeatures()] if model.layer else []
  97. self.sites_telecom = self.dataset.get(SiteTelecom, [])
  98. self.sites_client = self.dataset.get(SiteClient, [])
  99. self.cables = self.dataset.get(Cable, [])
  100. self.zapbos = self.dataset.get(Zapbo, [])
  101. self.zasros = self.dataset.get(Zasro, [])
  102. self.adductions = self.dataset.get(Adduction, [])
  103. def test_load_layers(self):
  104. """ Chargement des données
  105. Contrôle la présence des couches attendues
  106. """
  107. for model in models:
  108. if model.layer is None:
  109. self.log_error("Couche manquante", model=model)
  110. continue
  111. if model.pk:
  112. if not model.pk.lower() in [f.name().lower() for f in model.layer.fields()]:
  113. self.log_error(f"Clef primaire manquante ({model.pk})", model=model)
  114. continue
  115. def test_scr(self):
  116. """ Contrôle des projections
  117. Vérifie que les couches ont le bon sytème de projection
  118. """
  119. for model in models:
  120. if model.layer and model.layer.crs().authid() != model.crs:
  121. self.log_error(f"Mauvaise projection (attendu: {model.crs})", model=model)
  122. def _validate_structure(self, model, items):
  123. v = CerberusValidator(model.schema, purge_unknown=True, error_handler=CerberusErrorHandler, require_all=True)
  124. for item in items:
  125. v.validate(item.__dict__)
  126. for field, verrors in v.errors.items():
  127. for err in verrors:
  128. self.log_error(f"{field} : {_translate_messages(err)}", item=item)
  129. def test_structure_sites_telecom(self):
  130. """ Structure des données: SiteTelecom
  131. Contrôle les données attributaires de la couche SITE_TELECOM:
  132. présence, format, valeurs autorisées...
  133. """
  134. self._validate_structure(SiteTelecom, self.sites_telecom)
  135. def test_structure_sites_client(self):
  136. """ Structure des données: SiteClient
  137. Contrôle les données attributaires de la couche SITE_CLIENT:
  138. présence, format, valeurs autorisées...
  139. """
  140. self._validate_structure(SiteClient, self.sites_client)
  141. def test_structure_cables(self):
  142. """ Structure des données: Cables
  143. Contrôle les données attributaires de la couche CABLE:
  144. présence, format, valeurs autorisées...
  145. """
  146. self._validate_structure(Cable, self.cables)
  147. def test_structure_zapbos(self):
  148. """ Structure des données: Zapbo
  149. Contrôle les données attributaires de la couche ZAPBO:
  150. présence, format, valeurs autorisées...
  151. """
  152. self._validate_structure(Zapbo, self.zapbos)
  153. def test_structure_zasros(self):
  154. """ Structure des données: Zasro
  155. Contrôle les données attributaires de la couche ZASRO:
  156. présence, format, valeurs autorisées...
  157. """
  158. self._validate_structure(Zasro, self.zasros)
  159. def test_structure_adductions(self):
  160. """ Structure des données: Adduction
  161. Contrôle les données attributaires de la couche ADDUCTION:
  162. présence, format, valeurs autorisées...
  163. """
  164. self._validate_structure(Adduction, self.adductions)
  165. def test_geometry_validity(self):
  166. """ Contrôle de la validité des géométries
  167. """
  168. for model in models:
  169. for item in self.dataset[model]:
  170. if not item.is_geometry_valid():
  171. self.log_error("La géométrie de l'objet est invalide", item=item)
  172. def test_geometry_type(self):
  173. """ Contrôle des types de géométries
  174. """
  175. for model in models:
  176. for item in self.dataset[model]:
  177. item_geom_type = item.get_geom_type()
  178. if item_geom_type != model.geom_type:
  179. expected, found = model.get_geom_name(model.geom_type), model.get_geom_name(item_geom_type)
  180. self.log_error(f"Type de géométrie invalide (attendu: {expected}, trouvé: {found})", item=item)
  181. def test_bounding_box(self):
  182. """ Contrôle des emprises
  183. Vérifie que les objets sont dans le périmètre attendu
  184. """
  185. for model in models:
  186. xmin, ymin, xmax, ymax = model.bounding_box
  187. for item in self.dataset[model]:
  188. if not item.is_geometry_valid():
  189. continue
  190. x1, y1, x2, y2 = item.get_bounding_box()
  191. if any(x < xmin or x > xmax for x in (x1, x2)) or \
  192. any(y < ymin or y > ymax for y in (y1, y2)):
  193. self.log_error("Hors de l'emprise autorisée", item=item)
  194. def test_duplicates(self):
  195. """ Recherche de doublons
  196. Recherche d'éventuels doublons dans des champs qui supposent l'unicité
  197. """
  198. # doublons dans ST_CODE
  199. tmp = []
  200. for site_telecom in self.sites_telecom:
  201. if not site_telecom.ST_CODE:
  202. continue
  203. if not site_telecom.ST_CODE in tmp:
  204. tmp.append(site_telecom.ST_CODE)
  205. else:
  206. self.log_error("Doublons dans le champs ST_CODE", item=site_telecom)
  207. def test_graphic_duplicates(self):
  208. """ Recherche de doublons graphiques """
  209. for i, site_telecom in enumerate(self.sites_telecom):
  210. for other in self.sites_telecom[i+1:]:
  211. if site_telecom.geom.equals(other.geom):
  212. self.log_error("Une entité graphique est dupliquée", item=site_telecom)
  213. continue
  214. for i, site_client in enumerate(self.sites_client):
  215. for other in self.sites_client[i+1:]:
  216. if site_client.geom.equals(other.geom):
  217. self.log_error("Une entité graphique est dupliquée", item=site_client)
  218. continue
  219. # for i, cable in enumerate(self.cables):
  220. # for other in self.cables[i+1:]:
  221. # if cable.geom.equals(other.geom):
  222. # self.log_error("Une entité graphique est dupliquée", item=cable)
  223. # continue
  224. for i, zapbo in enumerate(self.zapbos):
  225. for other in self.zapbos[i+1:]:
  226. if zapbo.geom.equals(other.geom):
  227. self.log_error("Une entité graphique est dupliquée", item=zapbo)
  228. continue
  229. for i, zasro in enumerate(self.zasros):
  230. for other in self.zasros[i+1:]:
  231. if zasro.geom.equals(other.geom):
  232. self.log_error("Une entité graphique est dupliquée", item=zasro)
  233. continue
  234. # for i, adduction in enumerate(self.adductions):
  235. # for other in self.adductions[i+1:]:
  236. # if adduction.geom.equals(other.geom):
  237. # self.log_error("Une entité graphique est dupliquée", item=adduction)
  238. # continue
  239. def test_dimension_zasro(self):
  240. """ Contrôle le dimensionnement des ZASRO """
  241. for zasro in self.zasros:
  242. if zasro.ZS.NBPRISES > 850:
  243. self.log_error("Le nombre de prises est supérieur à 850", item=zasro)
  244. def test_affaiblissement(self):
  245. """ Contrôle l'affaiblissement """
  246. for site_client in self.site_clients:
  247. if site_client.ZS.NBPRISES > 28:
  248. self.log_error("L'affaiblissement est supérieur à 28", item=site_client)
  249. def test_capacite_modulo(self):
  250. """ Contrôle l'affaiblissement """
  251. for cable in self.cables:
  252. if cable.CA_CAPFO in [720,576,288,144,96,72] and cable.MODULO != "M12":
  253. self.log_error(f"Modulo invalide (capacite: {cable.CA_CAPFO}, modulo: {cable.MODULO})", item=cable)
  254. elif cable.CA_CAPFO in [72,48,24,12] and cable.MODULO != "M6":
  255. self.log_error(f"Modulo invalide (capacite: {cable.CA_CAPFO}, modulo: {cable.MODULO})", item=cable)
  256. def test_longueur_racco(self):
  257. """ Contrôle la longueur des raccos """
  258. for adduction in self.adductions:
  259. if adduction.AD_ISOLE == "1" and adduction.AD_LONG <= 100:
  260. self.log_error("L'adduction ne devrait pas être consiudérée comme isolée (long.: {adduction.AD_LONG})", item=adduction)
  261. elif adduction.AD_ISOLE == "0" and adduction.AD_LONG > 100:
  262. self.log_error("L'adduction devrait être consiudérée comme isolée (long.: {adduction.AD_LONG})", item=adduction)
  263. checkers = [Mn3ApdChecker]