mirror of
https://github.com/Alex38Lyon/Synthese-PSM_LARRA.git
synced 2026-09-30 07:21:55 +00:00
3546 lines
139 KiB
Python
3546 lines
139 KiB
Python
######!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
|
||
# Copyright (c) 2020 Xavier Robert <xavier.robert@ird.fr>
|
||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||
|
||
|
||
"""
|
||
!######################################################################
|
||
!# #
|
||
!# Script to automatize data extraction of Therion databases for QGis #
|
||
!# #
|
||
!# By Xavier Robert #
|
||
!# Grenoble, October 2022 #
|
||
!# #
|
||
!######################################################################
|
||
|
||
Written by Xavier Robert, October 2022
|
||
Xavier.robert@ird.fr
|
||
|
||
Modifié Alex 2025 01 31
|
||
Modifié Alex 2026 02 27
|
||
Modifié Alex 2026 08 28
|
||
|
||
Inputs files (28): (.dbf, .prj, .shp, .shx)
|
||
- points2d (4)
|
||
- lines2d (4)
|
||
- areas2d (4)
|
||
- outlines (4)
|
||
- shots3d (4)
|
||
- stations3d (4)
|
||
- walls3d (4)
|
||
|
||
Outputs files (8), in QGis_GPKG_Files folder:
|
||
- points2d.gpkg
|
||
- lines2dMasked.gpkg
|
||
- areas2dMasked.gpkg
|
||
- outline2d.gpkg
|
||
- shots3d.gpkg
|
||
- stations3d.gpkg
|
||
- walls3d.gpkg
|
||
- pyThtoQgis.log
|
||
|
||
|
||
En cas d'erreur (voir log), corriger manuellement avec QGis ou dans therion la topologie des fichiers
|
||
|
||
"""
|
||
|
||
from __future__ import division
|
||
|
||
import Lib.global_data as globalDat
|
||
from Lib.general_fonctions import setup_logger, Colors, safe_relpath, colored_help
|
||
|
||
|
||
# Import Python modules
|
||
import sys, os, argparse, time, math, logging, struct
|
||
import tkinter as tk
|
||
from tkinter import filedialog
|
||
from osgeo import ogr, gdal
|
||
from collections import defaultdict
|
||
from alive_progress import alive_bar # https://github.com/rsalmei/alive-progress
|
||
|
||
|
||
#################################################################################################
|
||
def cutGPKG(input_gpkg_path, outlines_path, output_gpkg_path):
|
||
"""
|
||
Generic clipping function for lines or polygons using OGR only.
|
||
|
||
Ne coupe que les objets de input_gpkg_path dont
|
||
_SCRAP_ID == _ID (dans outlines_path), et uniquement
|
||
avec la géométrie correspondante.
|
||
|
||
Args:
|
||
input_gpkg_path (str) : input gpkg path (lines or areas)
|
||
outlines_path (str) : polygon outline file (doit contenir _ID)
|
||
output_gpkg_path (str): output gpkg path
|
||
"""
|
||
try :
|
||
log.info(f"Clipping file : {Colors.ENDC}{safe_relpath(input_gpkg_path)}{Colors.INFO} to file : {Colors.ENDC}{safe_relpath(output_gpkg_path)}")
|
||
|
||
# -------------------------------------------------
|
||
# OPEN INPUT
|
||
# -------------------------------------------------
|
||
ds_in = ogr.Open(input_gpkg_path)
|
||
|
||
if ds_in is None:
|
||
log.error(f"cutGPKG, cannot open file : {Colors.ENDC}{safe_relpath(input_gpkg_path)}")
|
||
globalDat.errorCount += 1
|
||
return -1
|
||
|
||
layer_in = ds_in.GetLayer()
|
||
in_defn = layer_in.GetLayerDefn()
|
||
srs = layer_in.GetSpatialRef()
|
||
geom_type = layer_in.GetGeomType()
|
||
|
||
# Vérification présence champ _SCRAP_ID
|
||
idx_scrap = in_defn.GetFieldIndex("_SCRAP_ID")
|
||
|
||
if idx_scrap == -1:
|
||
log.error(f"cutGPKG, field {Colors.ENDC}'_SCRAP_ID'{Colors.ERROR} not found in input layer.")
|
||
globalDat.errorCount += 1
|
||
return -1
|
||
|
||
# -------------------------------------------------
|
||
# OPEN OUTLINES
|
||
# -------------------------------------------------
|
||
ds_outline = ogr.Open(outlines_path)
|
||
|
||
if ds_outline is None:
|
||
log.error(f"cutGPKG, cannot open file : {Colors.ENDC}{safe_relpath(outlines_path)}")
|
||
globalDat.errorCount += 1
|
||
return -1
|
||
|
||
layer_outline = ds_outline.GetLayer()
|
||
outline_defn = layer_outline.GetLayerDefn()
|
||
|
||
idx_id = outline_defn.GetFieldIndex("_ID")
|
||
|
||
if idx_id == -1:
|
||
log.error(f"cutGPKG, field {Colors.ENDC}'_ID'{Colors.ERROR} not found in outlines layer.")
|
||
globalDat.errorCount += 1
|
||
return -1
|
||
|
||
# -------------------------------------------------
|
||
# BUILD DICTIONARY {_ID : geometry}
|
||
# -------------------------------------------------
|
||
outline_dict = {}
|
||
|
||
for feat in layer_outline:
|
||
geom = feat.GetGeometryRef()
|
||
|
||
if geom is None:
|
||
continue
|
||
|
||
if not geom.IsValid():
|
||
geom = geom.Buffer(0)
|
||
|
||
scrap_id = feat.GetField("_ID")
|
||
|
||
if scrap_id is None:
|
||
continue
|
||
|
||
if scrap_id not in outline_dict:
|
||
outline_dict[scrap_id] = geom.Clone()
|
||
|
||
else:
|
||
outline_dict[scrap_id] = outline_dict[scrap_id].Union(geom)
|
||
|
||
if not outline_dict:
|
||
log.error("cutGPKG, no valid geometry found in outlines.")
|
||
globalDat.errorCount += 1
|
||
return -1
|
||
|
||
# -------------------------------------------------
|
||
# CREATE OUTPUT
|
||
# -------------------------------------------------
|
||
driver = ogr.GetDriverByName("GPKG")
|
||
|
||
if os.path.exists(output_gpkg_path):
|
||
driver.DeleteDataSource(output_gpkg_path)
|
||
|
||
ds_out = driver.CreateDataSource(output_gpkg_path)
|
||
|
||
out_layer = ds_out.CreateLayer(os.path.splitext(os.path.basename(output_gpkg_path))[0], srs=srs, geom_type=geom_type)
|
||
|
||
# Copy fields
|
||
for i in range(in_defn.GetFieldCount()):
|
||
out_layer.CreateField(in_defn.GetFieldDefn(i))
|
||
|
||
out_defn = out_layer.GetLayerDefn()
|
||
layer_in.ResetReading()
|
||
|
||
# -------------------------------------------------
|
||
# PROCESS FEATURES
|
||
# -------------------------------------------------
|
||
countObjetsInput = len(layer_in)
|
||
with alive_bar(countObjetsInput, title=f"{Colors.YELLOW}Clipping file {Colors.ENDC}{safe_relpath(input_gpkg_path)} {Colors.ENDC}", length=20) as bar:
|
||
for feat in layer_in:
|
||
|
||
geom = feat.GetGeometryRef()
|
||
|
||
if geom is None:
|
||
log.warning(f"geom is None")
|
||
bar()
|
||
continue
|
||
|
||
if not geom.IsValid():
|
||
geom = geom.Buffer(0)
|
||
|
||
scrap_id = feat.GetField("_SCRAP_ID")
|
||
|
||
if scrap_id not in outline_dict:
|
||
outline_dict[scrap_id] = geom.Clone()
|
||
|
||
# Si aucun scrap correspondant → on ignore
|
||
# if scrap_id not in outline_dict:
|
||
# code, error_type, error_msg = get_geometry_error(geom)
|
||
# attrs = []
|
||
|
||
# for i in range(in_defn.GetFieldCount()):
|
||
# field_name = in_defn.GetFieldDefn(i).GetNameRef()
|
||
# field_value = feat.GetField(i)
|
||
# attrs.append(f"{Colors.ENDC}{field_name}{Colors.WARNING}={Colors.ENDC}{field_value}{Colors.WARNING}")
|
||
|
||
# attrs_formatted = ', '.join(attrs)
|
||
|
||
# log.warning(f"Scrap_ID {Colors.ENDC}{scrap_id}{Colors.WARNING} issue, error type {Colors.ENDC}{error_type}{Colors.WARNING}, attributes: {Colors.ENDC}{attrs_formatted}{Colors.WARNING}")
|
||
# bar()
|
||
# continue
|
||
|
||
outline_geom = outline_dict[scrap_id]
|
||
|
||
_type = (feat.GetField("_TYPE") or "").strip().lower()
|
||
_clip = (feat.GetField("_CLIP") or "").strip().lower()
|
||
|
||
# -----------------------------------
|
||
# OUTSIDE (no clipping)
|
||
# -----------------------------------
|
||
keep_outside = (_type in {"label", "water_flow", "centerline"} or _clip == "off")
|
||
|
||
if keep_outside:
|
||
new_feat = ogr.Feature(out_defn)
|
||
new_feat.SetGeometry(geom.Clone())
|
||
|
||
for i in range(out_defn.GetFieldCount()):
|
||
new_feat.SetField(out_defn.GetFieldDefn(i).GetNameRef(), feat.GetField(i))
|
||
|
||
out_layer.CreateFeature(new_feat)
|
||
new_feat = None
|
||
bar()
|
||
continue
|
||
|
||
# Pas d'intersection → on ignore
|
||
if not geom.Intersects(outline_geom):
|
||
attrs = []
|
||
|
||
for i in range(in_defn.GetFieldCount()):
|
||
field_name = in_defn.GetFieldDefn(i).GetNameRef()
|
||
field_value = feat.GetField(i)
|
||
attrs.append(f"{Colors.ENDC}{field_name}{Colors.WARNING}={Colors.ENDC}{field_value}{Colors.WARNING}")
|
||
|
||
attrs_formatted = ', '.join(attrs)
|
||
|
||
log.warning(f"Any intersection issue, attributes: {Colors.ENDC}{attrs_formatted}{Colors.WARNING}")
|
||
bar()
|
||
continue
|
||
|
||
inter_geom = geom.Intersection(outline_geom)
|
||
|
||
if inter_geom is None or inter_geom.IsEmpty():
|
||
bar()
|
||
continue
|
||
|
||
new_feat = ogr.Feature(out_defn)
|
||
new_feat.SetGeometry(inter_geom)
|
||
|
||
for i in range(out_defn.GetFieldCount()):
|
||
new_feat.SetField(out_defn.GetFieldDefn(i).GetNameRef(), feat.GetField(i))
|
||
|
||
out_layer.CreateFeature(new_feat)
|
||
new_feat = None
|
||
bar()
|
||
|
||
# -------------------------------------------------
|
||
# CLEANUP
|
||
# -------------------------------------------------
|
||
ds_in = None
|
||
ds_outline = None
|
||
ds_out = None
|
||
|
||
return countObjetsInput
|
||
|
||
except RuntimeError as e:
|
||
log.error(f"cutGPKG in file {Colors.ENDC}{safe_relpath(outlines_path)}{Colors.ERROR}, unable to validate geometry: {Colors.ENDC}{e}{Colors.ERROR}, continuing anyway.")
|
||
globalDat.errorCount += 1
|
||
return -1
|
||
|
||
#################################################################################################
|
||
def extractVertices(input_gpkg_path, output_gpkg_path):
|
||
"""
|
||
Extract vertices from a line layer (GPKG) and write them as points into a GPKG.
|
||
|
||
Conditions :
|
||
- Ne conserve que les sommets dont M == 16
|
||
- Conserve tous les attributs d’origine
|
||
- Ajoute un attribut 'angle' correspondant à la direction locale de la ligne (en degrés)
|
||
- Si le fichier de sortie existe, les points sont ajoutés à la fin
|
||
"""
|
||
try :
|
||
|
||
log.info(f"Extract vertices from : {Colors.ENDC}{input_gpkg_path}{Colors.INFO} to {Colors.ENDC}{output_gpkg_path}")
|
||
|
||
# -------------------------------------------------
|
||
# OPEN INPUT
|
||
# -------------------------------------------------
|
||
ds_in = ogr.Open(input_gpkg_path)
|
||
if ds_in is None:
|
||
log.error(f"Extract vertices, cannot open file : {Colors.ENDC}{input_gpkg_path}")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
layer_in = ds_in.GetLayer()
|
||
in_defn = layer_in.GetLayerDefn()
|
||
srs = layer_in.GetSpatialRef()
|
||
|
||
geom_type = layer_in.GetGeomType()
|
||
|
||
allowed_types = {
|
||
0,
|
||
ogr.wkbLineString,
|
||
ogr.wkbMultiLineString,
|
||
ogr.wkbLineString25D,
|
||
ogr.wkbMultiLineString25D,
|
||
ogr.wkbLineStringM,
|
||
ogr.wkbMultiLineStringM,
|
||
ogr.wkbLineStringZM,
|
||
ogr.wkbMultiLineStringZM,
|
||
}
|
||
|
||
if geom_type not in allowed_types:
|
||
log.error(f"Extract vertices, layer must be LineString type with M support and not : {Colors.ENDC}{geom_type}.")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
# -------------------------------------------------
|
||
# CREATE OR OPEN OUTPUT
|
||
# -------------------------------------------------
|
||
driver = ogr.GetDriverByName("GPKG")
|
||
|
||
if os.path.exists(output_gpkg_path):
|
||
ds_out = ogr.Open(output_gpkg_path, update=1)
|
||
if ds_out is None:
|
||
log.error(f"Extract vertices, cannot open file : {Colors.ENDC}{output_gpkg_path}{Colors.ERROR} in update mode.")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
out_layer = ds_out.GetLayer()
|
||
out_defn = out_layer.GetLayerDefn()
|
||
|
||
else:
|
||
ds_out = driver.CreateDataSource(output_gpkg_path)
|
||
out_layer = ds_out.CreateLayer(os.path.splitext(os.path.basename(output_gpkg_path))[0], srs=srs, geom_type=ogr.wkbPoint25D )
|
||
|
||
|
||
# -------------------------------------------------
|
||
# COPY FIELDS (SAFE FOR EXISTING FILE)
|
||
# -------------------------------------------------
|
||
|
||
existing_defn = out_layer.GetLayerDefn()
|
||
|
||
exclude_fields = {
|
||
"fid",
|
||
"vertex_index",
|
||
"vertex_part",
|
||
"vertex_part_index",
|
||
"distance"
|
||
}
|
||
|
||
for i in range(in_defn.GetFieldCount()):
|
||
|
||
field_def = in_defn.GetFieldDefn(i)
|
||
field_name = field_def.GetNameRef()
|
||
|
||
if field_name.lower() in exclude_fields:
|
||
continue
|
||
|
||
# Si le champ existe déjà → on ne recrée pas
|
||
if existing_defn.GetFieldIndex(field_name) != -1:
|
||
continue
|
||
|
||
# Création sécurisée (sans Clone)
|
||
new_field = ogr.FieldDefn(field_name, field_def.GetType())
|
||
new_field.SetWidth(field_def.GetWidth())
|
||
new_field.SetPrecision(field_def.GetPrecision())
|
||
new_field.SetNullable(field_def.IsNullable())
|
||
|
||
out_layer.CreateField(new_field)
|
||
|
||
# Ajout du champ angle si absent
|
||
if existing_defn.GetFieldIndex("_TYPEFCR") == -1:
|
||
field_angle = ogr.FieldDefn("_TYPEFCR", ogr.OFTReal)
|
||
out_layer.CreateField(field_angle)
|
||
|
||
out_defn = out_layer.GetLayerDefn()
|
||
|
||
|
||
# -------------------------------------------------
|
||
# PROCESS
|
||
# -------------------------------------------------
|
||
layer_in.ResetReading()
|
||
|
||
with alive_bar(len(layer_in), title=f"{Colors.YELLOW}Extract vertices {Colors.ENDC}{input_gpkg_path}{Colors.ENDC}", length=20) as bar:
|
||
|
||
for feat in layer_in:
|
||
|
||
geom = feat.GetGeometryRef()
|
||
if geom is None:
|
||
continue
|
||
|
||
if not geom.IsValid():
|
||
geom = geom.Buffer(0)
|
||
|
||
def process_linestring(ls):
|
||
|
||
n = ls.GetPointCount()
|
||
if n < 2: return
|
||
|
||
for i in range(n):
|
||
x, y, z, m = ls.GetPointZM(i)
|
||
|
||
if m != 16: continue
|
||
|
||
# calcul direction locale
|
||
if i == 0:
|
||
x2, y2, _, _ = ls.GetPointZM(i + 1)
|
||
dx = x2 - x
|
||
dy = y2 - y
|
||
else:
|
||
x1, y1, _, _ = ls.GetPointZM(i - 1)
|
||
dx = x - x1
|
||
dy = y - y1
|
||
|
||
angle = math.degrees(math.atan2(dy, dx))
|
||
|
||
pt = ogr.Geometry(ogr.wkbPoint25D)
|
||
pt.AddPoint(x, y, z)
|
||
|
||
new_feat = ogr.Feature(out_defn)
|
||
new_feat.SetGeometry(pt)
|
||
|
||
# copie attributs
|
||
for f in range(in_defn.GetFieldCount()):
|
||
new_feat.SetField(in_defn.GetFieldDefn(f).GetNameRef(), feat.GetField(f) )
|
||
|
||
new_feat.SetField("_TYPEFCR", angle)
|
||
|
||
type_val = feat.GetField("_TYPE")
|
||
|
||
if type_val is not None:
|
||
new_feat.SetField("_TYPE", "line_" + str(type_val))
|
||
|
||
out_layer.CreateFeature(new_feat)
|
||
new_feat = None
|
||
|
||
geom_name = geom.GetGeometryName()
|
||
|
||
if geom_name == "LINESTRING":
|
||
process_linestring(geom)
|
||
|
||
elif geom_name == "MULTILINESTRING":
|
||
for part in range(geom.GetGeometryCount()):
|
||
process_linestring(geom.GetGeometryRef(part))
|
||
|
||
bar()
|
||
|
||
# -------------------------------------------------
|
||
# CLEANUP
|
||
# -------------------------------------------------
|
||
ds_in = None
|
||
ds_out = None
|
||
|
||
return
|
||
|
||
except RuntimeError as e:
|
||
log.error(f"extractVertices, unable to validate geometry: {e}, continuing anyway.")
|
||
globalDat.errorCount += 1
|
||
|
||
# ======================================================================
|
||
# FONCTION : ATTRIBUTS DIRECT
|
||
# ======================================================================
|
||
def get_dbf_attributes_direct(file_path, record_index):
|
||
"""
|
||
Lecture directe du DBF associé à un Shapefile.
|
||
|
||
Ne passe PAS par OGR et ne lit PAS le SHP.
|
||
Permet donc de récupérer les attributs d'un objet dont
|
||
la géométrie SHP est corrompue.
|
||
|
||
record_index :
|
||
index du record DBF à partir de 0.
|
||
Pour FID 46 => record_index = 46
|
||
"""
|
||
|
||
dbf_path = os.path.splitext(file_path)[0] + ".dbf"
|
||
|
||
try:
|
||
|
||
if not os.path.exists(dbf_path):
|
||
log.error(f"DBF introuvable : {Colors.ENDC}{safe_relpath(dbf_path)}" )
|
||
globalDat.errorCount += 1
|
||
return {}
|
||
|
||
with open(dbf_path, "rb") as f:
|
||
|
||
# ==========================================================
|
||
# EN-TETE DBF
|
||
# ==========================================================
|
||
|
||
header = f.read(32)
|
||
|
||
if len(header) != 32:
|
||
log.error(f"DBF invalide ou tronqué : {Colors.ENDC}{safe_relpath(dbf_path)}")
|
||
globalDat.errorCount += 1
|
||
return {}
|
||
|
||
# Nombre de records
|
||
num_records = struct.unpack("<I", header[4:8])[0]
|
||
|
||
# Taille de l'en-tête
|
||
header_length = struct.unpack("<H", header[8:10])[0]
|
||
|
||
# Taille d'un record
|
||
record_length = struct.unpack("<H", header[10:12])[0]
|
||
|
||
if record_index < 0 or record_index >= num_records:
|
||
log.error(f"Record DBF hors limites : {Colors.ENDC}{record_index}{Colors.ERROR} / {Colors.ENDC}{num_records}")
|
||
globalDat.errorCount += 1
|
||
return {}
|
||
|
||
# ==========================================================
|
||
# LECTURE DES DESCRIPTIONS DE CHAMPS
|
||
# ==========================================================
|
||
|
||
fields = []
|
||
|
||
while True:
|
||
descriptor = f.read(32)
|
||
|
||
if len(descriptor) != 32:
|
||
log.error("Fin inattendue des descripteurs DBF")
|
||
globalDat.errorCount += 1
|
||
return {}
|
||
|
||
# 0x0D = fin des descripteurs
|
||
if descriptor[0] == 0x0D:
|
||
break
|
||
|
||
# ------------------------------------------------------
|
||
# Nom du champ : 11 octets
|
||
# ------------------------------------------------------
|
||
|
||
field_name = (descriptor[0:11].split(b"\x00", 1)[0].decode("ascii", errors="replace").strip())
|
||
|
||
# Type
|
||
field_type = chr(descriptor[11])
|
||
|
||
# Taille
|
||
field_length = descriptor[16]
|
||
|
||
# Nombre de décimales
|
||
decimal_count = descriptor[17]
|
||
|
||
fields.append({"name": field_name, "type": field_type, "length": field_length, "decimals": decimal_count})
|
||
|
||
# ==========================================================
|
||
# POSITION DU RECORD
|
||
# ==========================================================
|
||
|
||
record_offset = (header_length+ record_index * record_length)
|
||
f.seek(record_offset)
|
||
record = f.read(record_length)
|
||
|
||
if len(record) != record_length:
|
||
log.error(f"Record DBF incomplet : FID {Colors.ENDC}{record_index}")
|
||
globalDat.errorCount += 1
|
||
return {}
|
||
|
||
# ==========================================================
|
||
# RECORD SUPPRIME ?
|
||
# ==========================================================
|
||
|
||
deletion_flag = record[0]
|
||
|
||
if deletion_flag == 0x2A: # '*'
|
||
log.warning(f"Record DBF {Colors.ENDC}{record_index}{Colors.WARNING} marqué comme supprimé")
|
||
|
||
# ==========================================================
|
||
# LECTURE DES VALEURS
|
||
# ==========================================================
|
||
|
||
attributes = {}
|
||
position = 1
|
||
|
||
for field in fields:
|
||
|
||
length = field["length"]
|
||
raw_value = record[position:position + length]
|
||
position += length
|
||
|
||
# ------------------------------------------------------
|
||
# Décodage
|
||
# ------------------------------------------------------
|
||
|
||
# On tente UTF-8 puis CP1252
|
||
try:
|
||
value = raw_value.decode("utf-8")
|
||
|
||
except UnicodeDecodeError:
|
||
value = raw_value.decode("cp1252", errors="replace")
|
||
|
||
value = value.strip()
|
||
|
||
# ------------------------------------------------------
|
||
# Conversion selon type DBF
|
||
# ------------------------------------------------------
|
||
|
||
field_type = field["type"]
|
||
|
||
if value == "":
|
||
value = None
|
||
|
||
elif field_type in ("N", "F"):
|
||
try:
|
||
if (field["decimals"] == 0 and "." not in value ):
|
||
value = int(value)
|
||
|
||
else:
|
||
value = float(value)
|
||
|
||
except ValueError:
|
||
pass
|
||
|
||
elif field_type == "L":
|
||
if value.upper() == "Y": value = True
|
||
elif value.upper() == "N": value = False
|
||
|
||
attributes[field["name"]] = value
|
||
|
||
return attributes
|
||
|
||
except Exception as e:
|
||
log.error(f"Erreur lecture directe DBF FID {Colors.ENDC}{record_index}{Colors.ERROR} : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
return {}
|
||
|
||
# ======================================================================
|
||
# FONCTION : AFFICHAGE ATTRIBUTS
|
||
# ======================================================================
|
||
def log_feature_attributes(attributes, level="debug"):
|
||
if level=="error":
|
||
log.error(f"Attributs de l'objet :")
|
||
if not attributes :
|
||
log.error("Attributs : indisponibles")
|
||
return
|
||
|
||
for field_name, value in attributes.items():
|
||
log.error(f"\t\t{Colors.ENDC}{field_name}{Colors.ERROR} = {Colors.ENDC}{value}")
|
||
|
||
else :
|
||
log.debug(f"Attributs de l'objet :")
|
||
if not attributes :
|
||
log.error("Attributs : indisponibles")
|
||
return
|
||
|
||
for field_name, value in attributes.items():
|
||
log.debug(f"\t\t{Colors.ENDC}{field_name}{Colors.DEBUG} = {Colors.ENDC}{value}")
|
||
|
||
#################################################################################################
|
||
def diagnosticNew(file_path):
|
||
"""
|
||
Diagnostic robuste d'un fichier vectoriel OGR/GDAL.
|
||
|
||
Analyse :
|
||
- nombre total de features
|
||
- géométries vides
|
||
- géométries invalides OGC
|
||
- géométries illisibles/corrompues
|
||
- géométries multiparties
|
||
- parties vides dans les multiparties
|
||
- structure des multiparties
|
||
- types géométriques
|
||
- présence Z / M
|
||
- emprise
|
||
- CRS
|
||
- statistiques des champs attributaires
|
||
|
||
En cas d'erreur GDAL sur un feature :
|
||
- l'erreur est enregistrée
|
||
- les attributs sont affichés lorsqu'ils sont accessibles
|
||
- le traitement du fichier continue autant que possible
|
||
|
||
Retour :
|
||
(invalid, total)
|
||
"""
|
||
|
||
start_time = time.time()
|
||
|
||
ds = None
|
||
total = 0
|
||
invalid = 0
|
||
empty = 0
|
||
geometry_read_errors = 0
|
||
geometry_validation_errors = 0
|
||
feature_errors = 0
|
||
multi_geom_count = 0
|
||
multi_geom_valid_count = 0
|
||
multi_geom_empty_part_count = 0
|
||
multi_geom_corrupted_count = 0
|
||
empty_part_count = 0
|
||
total_part_count = 0
|
||
geom_types = defaultdict(int)
|
||
has_z = False
|
||
has_m = False
|
||
field_stats = defaultdict(list)
|
||
error_codes = defaultdict(int)
|
||
error_types = defaultdict(int)
|
||
error_messages = defaultdict(int)
|
||
corrupted_features = []
|
||
field_definitions = []
|
||
|
||
# ======================================================================
|
||
# FONCTION : ATTRIBUTS DIRECT
|
||
# ======================================================================
|
||
def get_dbf_attributes_direct(file_path, record_index):
|
||
"""
|
||
Lecture directe du DBF associé à un Shapefile.
|
||
|
||
Ne passe PAS par OGR et ne lit PAS le SHP.
|
||
Permet donc de récupérer les attributs d'un objet dont
|
||
la géométrie SHP est corrompue.
|
||
|
||
record_index :
|
||
index du record DBF à partir de 0.
|
||
Pour FID 46 => record_index = 46
|
||
"""
|
||
|
||
dbf_path = os.path.splitext(file_path)[0] + ".dbf"
|
||
|
||
try:
|
||
|
||
if not os.path.exists(dbf_path):
|
||
log.error(f"DBF introuvable : {Colors.ENDC}{safe_relpath(dbf_path)}" )
|
||
globalDat.errorCount += 1
|
||
return {}
|
||
|
||
with open(dbf_path, "rb") as f:
|
||
|
||
# ==========================================================
|
||
# EN-TETE DBF
|
||
# ==========================================================
|
||
|
||
header = f.read(32)
|
||
|
||
if len(header) != 32:
|
||
log.error(f"DBF invalide ou tronqué : {Colors.ENDC}{safe_relpath(dbf_path)}")
|
||
globalDat.errorCount += 1
|
||
return {}
|
||
|
||
# Nombre de records
|
||
num_records = struct.unpack("<I", header[4:8])[0]
|
||
|
||
# Taille de l'en-tête
|
||
header_length = struct.unpack("<H", header[8:10])[0]
|
||
|
||
# Taille d'un record
|
||
record_length = struct.unpack("<H", header[10:12])[0]
|
||
|
||
if record_index < 0 or record_index >= num_records:
|
||
log.error(f"Record DBF hors limites : {Colors.ENDC}{record_index}{Colors.ERROR} / {Colors.ENDC}{num_records}")
|
||
globalDat.errorCount += 1
|
||
return {}
|
||
|
||
# ==========================================================
|
||
# LECTURE DES DESCRIPTIONS DE CHAMPS
|
||
# ==========================================================
|
||
|
||
fields = []
|
||
|
||
while True:
|
||
descriptor = f.read(32)
|
||
|
||
if len(descriptor) != 32:
|
||
log.error("Fin inattendue des descripteurs DBF")
|
||
globalDat.errorCount += 1
|
||
return {}
|
||
|
||
# 0x0D = fin des descripteurs
|
||
if descriptor[0] == 0x0D:
|
||
break
|
||
|
||
# ------------------------------------------------------
|
||
# Nom du champ : 11 octets
|
||
# ------------------------------------------------------
|
||
|
||
field_name = (descriptor[0:11].split(b"\x00", 1)[0].decode("ascii", errors="replace").strip())
|
||
|
||
# Type
|
||
field_type = chr(descriptor[11])
|
||
|
||
# Taille
|
||
field_length = descriptor[16]
|
||
|
||
# Nombre de décimales
|
||
decimal_count = descriptor[17]
|
||
|
||
fields.append({"name": field_name, "type": field_type, "length": field_length, "decimals": decimal_count})
|
||
|
||
# ==========================================================
|
||
# POSITION DU RECORD
|
||
# ==========================================================
|
||
|
||
record_offset = (header_length+ record_index * record_length)
|
||
f.seek(record_offset)
|
||
record = f.read(record_length)
|
||
|
||
if len(record) != record_length:
|
||
log.error(f"Record DBF incomplet : FID {Colors.ENDC}{record_index}")
|
||
globalDat.errorCount += 1
|
||
return {}
|
||
|
||
# ==========================================================
|
||
# RECORD SUPPRIME ?
|
||
# ==========================================================
|
||
|
||
deletion_flag = record[0]
|
||
|
||
if deletion_flag == 0x2A: # '*'
|
||
log.warning(f"Record DBF {Colors.ENDC}{record_index}{Colors.WARNING} marqué comme supprimé")
|
||
|
||
# ==========================================================
|
||
# LECTURE DES VALEURS
|
||
# ==========================================================
|
||
|
||
attributes = {}
|
||
position = 1
|
||
|
||
for field in fields:
|
||
|
||
length = field["length"]
|
||
raw_value = record[position:position + length]
|
||
position += length
|
||
|
||
# ------------------------------------------------------
|
||
# Décodage
|
||
# ------------------------------------------------------
|
||
|
||
# On tente UTF-8 puis CP1252
|
||
try:
|
||
value = raw_value.decode("utf-8")
|
||
|
||
except UnicodeDecodeError:
|
||
value = raw_value.decode("cp1252", errors="replace")
|
||
|
||
value = value.strip()
|
||
|
||
# ------------------------------------------------------
|
||
# Conversion selon type DBF
|
||
# ------------------------------------------------------
|
||
|
||
field_type = field["type"]
|
||
|
||
if value == "":
|
||
value = None
|
||
|
||
elif field_type in ("N", "F"):
|
||
try:
|
||
if (field["decimals"] == 0 and "." not in value ):
|
||
value = int(value)
|
||
|
||
else:
|
||
value = float(value)
|
||
|
||
except ValueError:
|
||
pass
|
||
|
||
elif field_type == "L":
|
||
if value.upper() == "Y": value = True
|
||
elif value.upper() == "N": value = False
|
||
|
||
attributes[field["name"]] = value
|
||
|
||
return attributes
|
||
|
||
except Exception as e:
|
||
log.error(f"Erreur lecture directe DBF FID {Colors.ENDC}{record_index}{Colors.ERROR} : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
return {}
|
||
|
||
# ======================================================================
|
||
# FONCTION : ATTRIBUTS
|
||
# ======================================================================
|
||
def get_feature_attributes(feature):
|
||
attributes = {}
|
||
|
||
if feature is None:
|
||
return attributes
|
||
|
||
try:
|
||
for field_index, field_name in field_definitions:
|
||
try:
|
||
value = feature.GetField(field_index)
|
||
attributes[field_name] = value
|
||
|
||
except Exception as e:
|
||
attributes[field_name] = (f"<ERREUR LECTURE : {Colors.ENDC}{e}>")
|
||
|
||
except Exception as e:
|
||
attributes["_ATTRIBUTE_ERROR_"] = str(e)
|
||
|
||
return attributes
|
||
|
||
# ======================================================================
|
||
# FONCTION : AFFICHAGE ATTRIBUTS
|
||
# ======================================================================
|
||
def log_feature_attributes(attributes, level="debug"):
|
||
if level=="error":
|
||
log.error(f"Attributs de l'objet :")
|
||
if not attributes :
|
||
log.error("Attributs : indisponibles")
|
||
return
|
||
|
||
for field_name, value in attributes.items():
|
||
log.error(f"\t\t{Colors.ENDC}{field_name}{Colors.ERROR} = {Colors.ENDC}{value}")
|
||
|
||
else :
|
||
log.debug(f"Attributs de l'objet :")
|
||
if not attributes :
|
||
log.error("Attributs : indisponibles")
|
||
return
|
||
|
||
for field_name, value in attributes.items():
|
||
log.debug(f"\t\t{Colors.ENDC}{field_name}{Colors.DEBUG} = {Colors.ENDC}{value}")
|
||
|
||
# ======================================================================
|
||
# FONCTION : ANALYSE MULTIPARTIE
|
||
# ======================================================================
|
||
def analyse_multipart_geometry(geom):
|
||
|
||
result = {
|
||
"is_multi": False,
|
||
"part_count": 0,
|
||
"empty_parts": 0,
|
||
"valid_structure": True,
|
||
"error": None
|
||
}
|
||
|
||
try:
|
||
geom_name = geom.GetGeometryName().upper()
|
||
|
||
except Exception as e:
|
||
|
||
result["valid_structure"] = False
|
||
result["error"] = str(e)
|
||
|
||
return result
|
||
|
||
multipart_types = (
|
||
"MULTIPOINT",
|
||
"MULTILINESTRING",
|
||
"MULTIPOLYGON",
|
||
"GEOMETRYCOLLECTION"
|
||
)
|
||
|
||
if geom_name not in multipart_types:
|
||
return result
|
||
|
||
result["is_multi"] = True
|
||
|
||
# --------------------------------------------------------------
|
||
# Nombre de parties
|
||
# --------------------------------------------------------------
|
||
|
||
try:
|
||
part_count = geom.GetGeometryCount()
|
||
result["part_count"] = part_count
|
||
|
||
except Exception as e:
|
||
result["valid_structure"] = False
|
||
result["error"] = (f"Impossible de récupérer le nombre de parties : {Colors.ENDC}{e}")
|
||
return result
|
||
|
||
# --------------------------------------------------------------
|
||
# Analyse de chaque partie
|
||
# --------------------------------------------------------------
|
||
|
||
for i in range(part_count):
|
||
try:
|
||
part = geom.GetGeometryRef(i)
|
||
|
||
except Exception as e:
|
||
result["valid_structure"] = False
|
||
if result["error"] is None:
|
||
result["error"] = (f"Impossible de lire la partie {Colors.ENDC}{i}{Colors.ERROR} : {Colors.ENDC}{e}")
|
||
continue
|
||
|
||
if part is None:
|
||
result["empty_parts"] += 1
|
||
continue
|
||
|
||
try:
|
||
if part.IsEmpty():
|
||
result["empty_parts"] += 1
|
||
|
||
except Exception as e:
|
||
result["valid_structure"] = False
|
||
|
||
if result["error"] is None:
|
||
result["error"] = (f"Impossible de tester la partie {Colors.ENDC}{i}{Colors.ERROR} : {Colors.ENDC}{e}")
|
||
|
||
return result
|
||
|
||
# ======================================================================
|
||
# FONCTION : AFFICHAGE DETAIL MULTIPARTIE
|
||
# ======================================================================
|
||
def log_multipart_details(geom, fid, geom_name, attributes ):
|
||
"""
|
||
Affiche le détail d'une géométrie multipartie :
|
||
- FID
|
||
- type
|
||
- nombre de parties
|
||
- attributs
|
||
- type et nombre de sommets de chaque partie
|
||
"""
|
||
|
||
try:
|
||
part_count = geom.GetGeometryCount()
|
||
|
||
except Exception as e:
|
||
log.error(f"GetGeometryCount, SHAPE {Colors.ENDC}{fid + 1}{Colors.ERROR} / FID {Colors.ENDC}{fid}{Colors.ERROR} : impossible de déterminer le nombre de parties : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
log.debug(f"MultiGeometry détectée : SHAPE {Colors.ENDC}{fid + 1}{Colors.DEBUG} / FID {Colors.ENDC}{fid}")
|
||
log.debug(f"\t\tType : {Colors.ENDC}{geom_name}")
|
||
log.debug(f"\t\tNombre de parties : {Colors.ENDC}{part_count}")
|
||
|
||
# --------------------------------------------------------------
|
||
# Attributs
|
||
# --------------------------------------------------------------
|
||
log_feature_attributes(attributes)
|
||
|
||
# --------------------------------------------------------------
|
||
# Parties
|
||
# --------------------------------------------------------------
|
||
|
||
for part_index in range(part_count):
|
||
try:
|
||
part = geom.GetGeometryRef(part_index)
|
||
if part is None:
|
||
log.warning(f"\t\tPartie {Colors.ENDC}{part_index + 1}{Colors.WARNING} : NULL / VIDE")
|
||
continue
|
||
|
||
try:
|
||
part_type = part.GetGeometryName()
|
||
|
||
except Exception:
|
||
part_type = "UNKNOWN"
|
||
|
||
try:
|
||
point_count = part.GetPointCount()
|
||
|
||
except Exception:
|
||
point_count = "?"
|
||
|
||
try:
|
||
is_empty = part.IsEmpty()
|
||
|
||
except Exception:
|
||
is_empty = "?"
|
||
|
||
if is_empty is True:
|
||
log.warning(f"\t\tPartie {Colors.ENDC}{part_index + 1}{Colors.WARNING} : VIDE")
|
||
|
||
else:
|
||
log.debug(f"\t\tPartie {Colors.ENDC}{part_index + 1}{Colors.DEBUG} : {Colors.ENDC}{part_type}{Colors.DEBUG}, {Colors.ENDC}{point_count}{Colors.DEBUG} sommets")
|
||
|
||
except Exception as e:
|
||
log.error(f"\t\tPartie {Colors.ENDC}{part_index + 1}{Colors.ERROR} : erreur lecture : {Colors.ENDC}{e}")
|
||
|
||
|
||
# ======================================================================
|
||
# OUVERTURE
|
||
# ======================================================================
|
||
try:
|
||
log.info(f"==================== BILAN FILE: {Colors.ENDC}{safe_relpath(file_path)}{Colors.INFO} ===================================")
|
||
|
||
# ==================================================================
|
||
# EXISTENCE
|
||
# ==================================================================
|
||
|
||
if not os.path.exists(file_path):
|
||
log.error(f"diagnostic, fichier non trouvé : {Colors.ENDC}{safe_relpath(file_path)}")
|
||
globalDat.errorCount += 1
|
||
return -1, -1
|
||
|
||
# ==================================================================
|
||
# OGR OPEN
|
||
# ==================================================================
|
||
|
||
try:
|
||
ds = ogr.Open(file_path)
|
||
|
||
except Exception as e:
|
||
log.error(f"Impossible d'ouvrir le fichier : {Colors.ENDC}{safe_relpath(file_path)}{Colors.ERROR} : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
return -1, -1
|
||
|
||
if ds is None:
|
||
log.error(f"Impossible d'ouvrir le fichier : {Colors.ENDC}{safe_relpath(file_path)}")
|
||
globalDat.errorCount += 1
|
||
return -1, -1
|
||
|
||
# ==================================================================
|
||
# LAYER
|
||
# ==================================================================
|
||
try:
|
||
layer = ds.GetLayer()
|
||
|
||
except Exception as e:
|
||
log.error(f"Impossible de récupérer la couche : {Colors.ENDC}{safe_relpath(file_path)}{Colors.ERROR} : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
return -1, -1
|
||
|
||
if layer is None:
|
||
log.error(f"Couche inaccessible : {Colors.ENDC}{safe_relpath(file_path)}")
|
||
globalDat.errorCount += 1
|
||
return -1, -1
|
||
|
||
# ==================================================================
|
||
# NOMBRE DE FEATURES
|
||
# ==================================================================
|
||
try:
|
||
feature_count = layer.GetFeatureCount()
|
||
|
||
except Exception as e:
|
||
feature_count = -1
|
||
log.warning(f"Impossible de déterminer le nombre de features : {Colors.ENDC}{e}")
|
||
|
||
log.debug(f"Nombre de features annoncé par GDAL : {Colors.ENDC}{feature_count}")
|
||
|
||
# ==================================================================
|
||
# EXTENT
|
||
# ==================================================================
|
||
try:
|
||
extent = layer.GetExtent()
|
||
|
||
except Exception as e:
|
||
extent = None
|
||
log.warning(f"Impossible de récupérer l'emprise : {Colors.ENDC}{e}")
|
||
|
||
# ==================================================================
|
||
# CRS
|
||
# ==================================================================
|
||
try:
|
||
srs = layer.GetSpatialRef()
|
||
crs = (srs.ExportToWkt() if srs else "CRS inconnu" )
|
||
|
||
except Exception as e:
|
||
crs = "CRS inconnu"
|
||
log.warning(f"Impossible de récupérer le CRS : {Colors.ENDC}{e}")
|
||
|
||
# ==================================================================
|
||
# CHAMPS
|
||
# ==================================================================
|
||
try:
|
||
layer_defn = layer.GetLayerDefn()
|
||
|
||
for i in range(layer_defn.GetFieldCount()):
|
||
field_definitions.append((i, layer_defn.GetFieldDefn(i).GetNameRef()))
|
||
|
||
except Exception as e:
|
||
log.warning(f"Impossible de récupérer les champs : {Colors.ENDC}{e}")
|
||
|
||
# ==================================================================
|
||
# PARCOURS PAR FID
|
||
# ==================================================================
|
||
|
||
if feature_count >= 0:
|
||
for fid in range(feature_count):
|
||
# ----------------------------------------------------------
|
||
# Lecture du feature
|
||
# ----------------------------------------------------------
|
||
try:
|
||
feature = layer.GetFeature(fid)
|
||
|
||
except Exception as e:
|
||
geometry_read_errors += 1
|
||
attributes = get_dbf_attributes_direct(file_path, fid)
|
||
corrupted_features.append({"index": total + 1, "fid": fid, "shape": fid + 1, "stage": "GetFeature", "error": str(e), "attributes": attributes})
|
||
|
||
log.error(f"GetFeature, SHAPE : {Colors.ENDC}{fid + 1}{Colors.ERROR}, FID : {Colors.ENDC}{fid}{Colors.ERROR}, GetFeature : {Colors.ENDC}{e}" )
|
||
globalDat.errorCount += 1
|
||
log_feature_attributes(attributes, "error")
|
||
total += 1
|
||
continue
|
||
|
||
total += 1
|
||
|
||
if feature is None:
|
||
geometry_read_errors += 1
|
||
corrupted_features.append({"index": total, "fid": fid, "shape": fid + 1, "stage": "GetFeature", "error": "Feature None", "attributes": {}})
|
||
log.error(f"SHAPE {Colors.ENDC}{fid + 1}{Colors.ERROR} / FID {Colors.ENDC}{fid}{Colors.ERROR} : {Colors.ENDC}GDAL retourne None")
|
||
globalDat.errorCount += 1
|
||
continue
|
||
|
||
# ----------------------------------------------------------
|
||
# Attributs
|
||
# ----------------------------------------------------------
|
||
attributes = get_feature_attributes(feature)
|
||
|
||
# ----------------------------------------------------------
|
||
# Géométrie
|
||
# ----------------------------------------------------------
|
||
try:
|
||
geom = feature.GetGeometryRef()
|
||
|
||
except Exception as e:
|
||
geometry_read_errors += 1
|
||
corrupted_features.append({"index": total, "fid": fid, "shape": fid + 1, "stage": "GetGeometryRef", "error": str(e), "attributes": attributes})
|
||
log.error(f"SHAPE {Colors.ENDC}{fid + 1}{Colors.ERROR} / FID {Colors.ENDC}{fid}{Colors.ERROR} : {Colors.ENDC}géométrie illisible{Colors.ERROR} : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
log_feature_attributes(attributes)
|
||
continue
|
||
|
||
# ----------------------------------------------------------
|
||
# Géométrie absente
|
||
# ----------------------------------------------------------
|
||
if geom is None:
|
||
empty += 1
|
||
continue
|
||
|
||
# ----------------------------------------------------------
|
||
# Géométrie vide
|
||
# ----------------------------------------------------------
|
||
try:
|
||
if geom.IsEmpty():
|
||
empty += 1
|
||
continue
|
||
|
||
except Exception as e:
|
||
geometry_read_errors += 1
|
||
corrupted_features.append({"index": total, "fid": fid, "shape": fid + 1, "stage": "IsEmpty", "error": str(e), "attributes": attributes })
|
||
log.error(f"IsEmpty, SHAPE {Colors.ENDC}{fid + 1}{Colors.ERROR} / FID {Colors.ENDC}{fid}{Colors.ERROR} : impossible de tester IsEmpty() : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
log_feature_attributes(attributes)
|
||
continue
|
||
|
||
# ----------------------------------------------------------
|
||
# Type
|
||
# ----------------------------------------------------------
|
||
try:
|
||
geom_name = geom.GetGeometryName()
|
||
geom_types[geom_name] += 1
|
||
|
||
except Exception as e:
|
||
geom_name = "UNKNOWN"
|
||
log.warning(f"GetGeometryName UNKNOWN, SHAPE {Colors.ENDC}{fid + 1}{Colors.ENDC} / FID {Colors.ENDC}{fid} : impossible de lire le type : {Colors.ENDC}{e}")
|
||
|
||
# ==========================================================
|
||
# ANALYSE MULTIPARTIE
|
||
# ==========================================================
|
||
try:
|
||
multipart = analyse_multipart_geometry(geom)
|
||
|
||
if multipart["is_multi"]:
|
||
multi_geom_count += 1
|
||
part_count = multipart["part_count"]
|
||
total_part_count += part_count
|
||
empty_parts = multipart["empty_parts"]
|
||
|
||
# --------------------------------------------------
|
||
# Affichage détaillé
|
||
# --------------------------------------------------
|
||
log_multipart_details(geom, fid, geom_name, attributes)
|
||
|
||
# --------------------------------------------------
|
||
# Multipartie normale
|
||
# --------------------------------------------------
|
||
|
||
if (multipart["valid_structure"] and empty_parts == 0 ):
|
||
multi_geom_valid_count += 1
|
||
|
||
# --------------------------------------------------
|
||
# Multipartie avec partie vide
|
||
# --------------------------------------------------
|
||
|
||
elif empty_parts > 0:
|
||
multi_geom_empty_part_count += 1
|
||
empty_part_count += empty_parts
|
||
log.warning(f"SHAPE {Colors.ENDC}{fid + 1}{Colors.WARNING} / FID {Colors.ENDC}{fid}{Colors.WARNING} : {Colors.ENDC}MULTIPARTIE AVEC PARTIE VIDE")
|
||
log.warning(f"\t\tNombre de parties : {Colors.ENDC}{part_count}")
|
||
log.warning(f"\t\tParties vides : {Colors.ENDC}{empty_parts}")
|
||
|
||
# --------------------------------------------------
|
||
# Structure non analysable
|
||
# --------------------------------------------------
|
||
|
||
if not multipart["valid_structure"]:
|
||
multi_geom_corrupted_count += 1
|
||
log.error(f"SHAPE {Colors.ENDC}{fid + 1}{Colors.ERROR} / FID {Colors.ENDC}{fid}{Colors.ERROR} : {Colors.ENDC}structure multipartie non analysable")
|
||
log.error(f"\t\tcode : {Colors.ENDC}{multipart['error']}")
|
||
globalDat.errorCount += 1
|
||
|
||
except Exception as e:
|
||
multi_geom_corrupted_count += 1
|
||
log.error(f"SHAPE {Colors.ENDC}{fid + 1}{Colors.ERROR} / FID {Colors.ENDC}{fid}{Colors.ERROR} : {Colors.ENDC}erreur analyse multipartie : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
|
||
# ==========================================================
|
||
# VALIDATION OGC
|
||
# ==========================================================
|
||
try:
|
||
valid = geom.IsValid()
|
||
|
||
except Exception as e:
|
||
geometry_validation_errors += 1
|
||
corrupted_features.append({ "index": total, "fid": fid, "shape": fid + 1, "stage": "IsValid", "error": str(e), "attributes": attributes})
|
||
|
||
log.error(f"SHAPE {Colors.ENDC}{fid + 1}{Colors.ERROR} / FID {Colors.ENDC}{fid}{Colors.ERROR} : impossible de valider la géométrie : {Colors.ENDC}{e}")
|
||
log_feature_attributes(attributes)
|
||
globalDat.errorCount += 1
|
||
continue
|
||
|
||
# ----------------------------------------------------------
|
||
# Géométrie invalide
|
||
# ----------------------------------------------------------
|
||
if not valid:
|
||
invalid += 1
|
||
|
||
try:
|
||
code, error_type, error_msg = (get_geometry_error(geom))
|
||
error_codes[code] += 1
|
||
error_types[error_type] += 1
|
||
error_messages[error_msg] += 1
|
||
|
||
except Exception:
|
||
error_types["Unknown"] += 1
|
||
|
||
# ==========================================================
|
||
# Z / M
|
||
# ==========================================================
|
||
try:
|
||
gtype = geom.GetGeometryType()
|
||
|
||
if ogr.GT_HasZ(gtype): has_z = True
|
||
|
||
if ogr.GT_HasM(gtype): has_m = True
|
||
|
||
except Exception as e:
|
||
log.warning(f"SHAPE {Colors.ENDC}{fid + 1}{Colors.WARNING} / FID {Colors.ENDC}{fid}{Colors.WARNING} : impossible de déterminer Z/M : {Colors.ENDC}{e}")
|
||
|
||
# ==========================================================
|
||
# STATISTIQUES ATTRIBUTAIRES
|
||
# ==========================================================
|
||
try:
|
||
for field_index, field_name in field_definitions:
|
||
value = feature.GetField(field_index)
|
||
|
||
if value is not None:
|
||
field_stats[field_name].append(value)
|
||
|
||
except Exception as e:
|
||
feature_errors += 1
|
||
log.warning(f"SHAPE {Colors.ENDC}{fid + 1}{Colors.WARNING} / FID {Colors.ENDC}{fid}{Colors.WARNING} : erreur lecture attributs : {Colors.ENDC}{e}")
|
||
continue
|
||
|
||
else:
|
||
|
||
# ==================================================================
|
||
# FALLBACK : PARCOURS SEQUENTIEL
|
||
# ==================================================================
|
||
log.warning("Nombre de features inconnu : utilisation du parcours séquentiel.")
|
||
|
||
while True:
|
||
try:
|
||
feature = layer.GetNextFeature()
|
||
|
||
except Exception as e:
|
||
geometry_read_errors += 1
|
||
log.error(f"Erreur GDAL lors de la lecture du prochain feature du fichier {Colors.ENDC}{safe_relpath(file_path)}{Colors.ERROR} : {Colors.ENDC}{e}")
|
||
log.error("Arrêt du parcours des features pour ce fichier, mais poursuite du traitement global.")
|
||
globalDat.errorCount += 1
|
||
break
|
||
|
||
if feature is None:
|
||
break
|
||
|
||
total += 1
|
||
|
||
try:
|
||
fid = feature.GetFID()
|
||
|
||
except Exception:
|
||
fid = "?"
|
||
|
||
attributes = get_feature_attributes(feature)
|
||
|
||
try:
|
||
geom = feature.GetGeometryRef()
|
||
|
||
if geom is None or geom.IsEmpty():
|
||
empty += 1
|
||
continue
|
||
|
||
geom_name = geom.GetGeometryName()
|
||
geom_types[geom_name] += 1
|
||
|
||
except Exception as e:
|
||
geometry_read_errors += 1
|
||
corrupted_features.append(
|
||
{
|
||
"index": total,
|
||
"fid": fid,
|
||
"shape": (
|
||
fid + 1
|
||
if isinstance(fid, int)
|
||
else "?"
|
||
),
|
||
"stage": "Geometry",
|
||
"error": str(e),
|
||
"attributes": attributes
|
||
}
|
||
)
|
||
|
||
log.error(f"Feature {Colors.ENDC}{total}{Colors.ERROR} / FID {Colors.ENDC}{fid}{Colors.ERROR} : erreur géométrie : {Colors.ENDC}{e}")
|
||
log_feature_attributes(attributes)
|
||
globalDat.errorCount += 1
|
||
continue
|
||
|
||
# --------------------------------------------------------------
|
||
# Multipartie
|
||
# --------------------------------------------------------------
|
||
try:
|
||
multipart = analyse_multipart_geometry(geom)
|
||
|
||
if multipart["is_multi"]:
|
||
multi_geom_count += 1
|
||
total_part_count += (multipart["part_count"])
|
||
log_multipart_details(geom, fid, geom_name, attributes)
|
||
|
||
if multipart["empty_parts"] > 0:
|
||
multi_geom_empty_part_count += 1
|
||
empty_part_count += (multipart["empty_parts"])
|
||
|
||
elif multipart["valid_structure"]:
|
||
multi_geom_valid_count += 1
|
||
|
||
else:
|
||
multi_geom_corrupted_count += 1
|
||
|
||
except Exception as e:
|
||
multi_geom_corrupted_count += 1
|
||
log.error(f"Feature {Colors.ENDC}{total}{Colors.ERROR} / FID {Colors.ENDC}{fid}{Colors.ERROR} : erreur analyse multipartie : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
|
||
# --------------------------------------------------------------
|
||
# Validation
|
||
# --------------------------------------------------------------
|
||
try:
|
||
if not geom.IsValid():
|
||
invalid += 1
|
||
|
||
try:
|
||
code, error_type, error_msg = ( get_geometry_error(geom))
|
||
error_codes[code] += 1
|
||
error_types[error_type] += 1
|
||
error_messages[error_msg] += 1
|
||
|
||
except Exception:
|
||
error_types["Unknown"] += 1
|
||
|
||
except Exception as e:
|
||
geometry_validation_errors += 1
|
||
corrupted_features.append(
|
||
{
|
||
"index": total,
|
||
"fid": fid,
|
||
"shape": (
|
||
fid + 1
|
||
if isinstance(fid, int)
|
||
else "?"
|
||
),
|
||
"stage": "IsValid",
|
||
"error": str(e),
|
||
"attributes": attributes
|
||
}
|
||
)
|
||
|
||
log.error(f"Feature {Colors.ENDC}{total}{Colors.ERROR} / FID {Colors.ENDC}{fid}{Colors.ERROR} : impossible de valider : {Colors.ENDC}{e}")
|
||
log_feature_attributes(attributes)
|
||
globalDat.errorCount += 1
|
||
continue
|
||
|
||
# --------------------------------------------------------------
|
||
# Z / M
|
||
# --------------------------------------------------------------
|
||
try:
|
||
gtype = geom.GetGeometryType()
|
||
|
||
if ogr.GT_HasZ(gtype): has_z = True
|
||
if ogr.GT_HasM(gtype): has_m = True
|
||
|
||
except Exception:
|
||
pass
|
||
|
||
# --------------------------------------------------------------
|
||
# Attributs
|
||
# --------------------------------------------------------------
|
||
try:
|
||
for field_index, field_name in field_definitions:
|
||
value = feature.GetField(field_index)
|
||
if value is not None:
|
||
field_stats[field_name].append(value)
|
||
|
||
except Exception as e :
|
||
feature_errors += 1
|
||
|
||
# ==================================================================
|
||
# BILAN
|
||
# ==================================================================
|
||
|
||
elapsed = time.time() - start_time
|
||
|
||
try:
|
||
file_size = (os.path.getsize(file_path)/(1024 * 1024))
|
||
|
||
except Exception:
|
||
file_size = 0
|
||
|
||
|
||
log.debug(f"Temps d'analyse : {Colors.ENDC}{elapsed:.2f}{Colors.DEBUG} s")
|
||
log.debug(f"Taille : {Colors.ENDC}{file_size:.2f}{Colors.DEBUG} Mo")
|
||
log.debug(f"Nombre d'objets annoncés par GDAL : {Colors.ENDC}{feature_count}")
|
||
log.debug(f"Nombre d'objets analysés : {Colors.ENDC}{total}")
|
||
|
||
# ==================================================================
|
||
# VIDES
|
||
# ==================================================================
|
||
if empty == 0:
|
||
log.debug(f"Géométries vides : {Colors.ENDC}{empty}")
|
||
|
||
else:
|
||
log.warning(f"Géométries vides : {Colors.ENDC}{empty}")
|
||
|
||
# ==================================================================
|
||
# INVALIDES
|
||
# ==================================================================
|
||
if invalid == 0:
|
||
log.info(f"Géométries invalides : {Colors.ENDC}{invalid}{Colors.INFO} sur {Colors.ENDC}{total}")
|
||
|
||
else:
|
||
log.warning(f"Géométries invalides : {Colors.ENDC}{invalid}{Colors.WARNING} sur {Colors.ENDC}{total}")
|
||
|
||
for error, count in sorted(error_types.items(), key=lambda item: item[1], reverse=True ):
|
||
log.warning(f"\t\t{Colors.ENDC}{error}{Colors.WARNING} : {Colors.ENDC}{count}")
|
||
|
||
# ==================================================================
|
||
# CORROMPU / NON ANALYSABLE
|
||
# ==================================================================
|
||
|
||
corrupted_count = ( geometry_read_errors + geometry_validation_errors )
|
||
|
||
if corrupted_count == 0:
|
||
log.info(f"Géométries corrompues / non analysables : {Colors.ENDC}0")
|
||
|
||
else:
|
||
log.error(f"Géométries corrompues / non analysables : {Colors.ENDC}{corrupted_count}")
|
||
|
||
for item in corrupted_features:
|
||
log.error(f"SHAPE {Colors.ENDC}{item['shape']}{Colors.ERROR} / FID {Colors.ENDC}{item['fid']}{Colors.ERROR} / {Colors.ENDC}{item['stage']}{Colors.ERROR} : {Colors.ENDC}{item['error']}")
|
||
log_feature_attributes(item["attributes"])
|
||
|
||
# ==================================================================
|
||
# MULTIPARTIES
|
||
# ==================================================================
|
||
log.debug(f"MultiGeometries / Collections : {Colors.ENDC}{multi_geom_count}")
|
||
log.debug(f"MultiGeometries avec structure valide : {Colors.ENDC}{multi_geom_valid_count}")
|
||
log.debug(f"MultiGeometries avec partie(s) vide(s) : {Colors.ENDC}{multi_geom_empty_part_count}")
|
||
log.debug(f"MultiGeometries avec structure non analysable : {Colors.ENDC}{multi_geom_corrupted_count}")
|
||
log.debug(f"Nombre total de parties multiparties : {Colors.ENDC}{total_part_count}")
|
||
|
||
if multi_geom_count > 0:
|
||
log.debug(f"Nombre moyen de parties par MultiGeometry : {Colors.ENDC}{total_part_count / multi_geom_count:.2f}")
|
||
|
||
if empty_part_count > 0:
|
||
log.warning(f"Nombre total de parties vides : {Colors.ENDC}{empty_part_count}")
|
||
|
||
# ==================================================================
|
||
# ERREURS
|
||
# ==================================================================
|
||
log.debug(f"Erreurs lecture géométrie : {Colors.ENDC}{geometry_read_errors}")
|
||
log.debug(f"Erreurs validation géométrie : {Colors.ENDC}{geometry_validation_errors}")
|
||
log.debug(f"Erreurs lecture attributs : {Colors.ENDC}{feature_errors}")
|
||
|
||
# ==================================================================
|
||
# TYPES
|
||
# ==================================================================
|
||
log.debug("Types géométriques :")
|
||
|
||
for gtype, count in geom_types.items():
|
||
log.debug(f"\t\t{Colors.ENDC}{gtype}{Colors.DEBUG} : {Colors.ENDC}{count}")
|
||
|
||
# ==================================================================
|
||
# EXTENT
|
||
# ==================================================================
|
||
log.debug("Bounding box :")
|
||
|
||
if extent is not None:
|
||
try:
|
||
log.debug(f"\t\txmin = {Colors.ENDC}{extent[0]:>8.3f}{Colors.DEBUG}\txmax = {Colors.ENDC}{extent[1]:>8.3f}")
|
||
log.debug(f"\t\tymin = {Colors.ENDC}{extent[2]:>8.3f}{Colors.DEBUG}\tymax = {Colors.ENDC}{extent[3]:>8.3f}")
|
||
|
||
except Exception:
|
||
log.debug(f"\t\tEmprise : {Colors.ENDC}{extent}")
|
||
|
||
else:
|
||
log.debug(f"\t\tEmprise indisponible")
|
||
|
||
# ==================================================================
|
||
# CRS
|
||
# ==================================================================
|
||
log.debug(f"CRS : {Colors.ENDC}{crs}")
|
||
|
||
# ==================================================================
|
||
# Z / M
|
||
# ==================================================================
|
||
log.debug(f"Dimensions, Z présent : {Colors.ENDC}{has_z}{Colors.DEBUG}\tM présent : {Colors.ENDC}{has_m}")
|
||
|
||
# ==================================================================
|
||
# CHAMPS
|
||
# ==================================================================
|
||
log.debug("Champs attributaires :")
|
||
|
||
for field, values in field_stats.items():
|
||
try:
|
||
unique_count = len(set(values))
|
||
|
||
except Exception:
|
||
unique_count = "?"
|
||
|
||
field_width = 20
|
||
number_width = 10
|
||
log.debug(f"\t\t{Colors.ENDC}{field:<{field_width}}{Colors.DEBUG}: {Colors.ENDC}{len(values):>{number_width}}{Colors.DEBUG} valeurs, {Colors.ENDC}{str(unique_count):>{number_width}}{Colors.DEBUG} uniques")
|
||
|
||
# ==================================================================
|
||
# BILAN FINAL
|
||
# ==================================================================
|
||
log.info(f"BILAN : {Colors.ENDC}{total}{Colors.INFO} objets, {Colors.ENDC}{invalid}{Colors.INFO} invalides, {Colors.ENDC}{empty}{Colors.INFO} vides, {Colors.ENDC}{corrupted_count}{Colors.INFO} corrompus/non analysables, {Colors.ENDC}{multi_geom_count}{Colors.INFO} multiparties")
|
||
log.info(f"=========================================================================================================")
|
||
|
||
return invalid, total
|
||
|
||
# ======================================================================
|
||
# ERREUR GLOBALE
|
||
# ======================================================================
|
||
except Exception as e:
|
||
log.error(f"diagnostic file: {Colors.ENDC}{safe_relpath(file_path)}{Colors.ERROR}, erreur inattendue : {Colors.ENDC}{e}{Colors.ERROR}, continuing anyway.")
|
||
globalDat.errorCount += 1
|
||
return -1, -1
|
||
|
||
# ======================================================================
|
||
# FERMETURE
|
||
# ======================================================================
|
||
finally:
|
||
ds = None
|
||
|
||
def diagnostic(file_path):
|
||
total = 0
|
||
invalid = 0
|
||
empty = 0
|
||
multi_geom_count = 0
|
||
geometry_read_errors = 0
|
||
geom_types = defaultdict(int)
|
||
has_z = False
|
||
has_m = False
|
||
field_stats = defaultdict(list)
|
||
|
||
error_codes = defaultdict(int)
|
||
error_types = defaultdict(int)
|
||
error_messages = defaultdict(int)
|
||
corrupted_features = []
|
||
|
||
try:
|
||
start_time = time.time()
|
||
|
||
log.info(f"==================== BILAN FILE: {Colors.ENDC}{safe_relpath(file_path)}{Colors.INFO} ===================================")
|
||
|
||
if not os.path.exists(file_path):
|
||
log.error(f"diagnostic, fichier non trouvé : {Colors.ENDC}{safe_relpath(file_path)}")
|
||
globalDat.errorCount += 1
|
||
return -1, -1
|
||
|
||
|
||
ds = ogr.Open(file_path)
|
||
|
||
|
||
if ds is None:
|
||
log.error(f"Impossible d'ouvrir le fichier : {Colors.ENDC}{safe_relpath(file_path)}")
|
||
globalDat.errorCount += 1
|
||
return -1, -1
|
||
|
||
layer = ds.GetLayer()
|
||
|
||
extent = layer.GetExtent()
|
||
srs = layer.GetSpatialRef()
|
||
crs = srs.ExportToWkt() if srs else "CRS inconnu"
|
||
|
||
feature_count = layer.GetFeatureCount()
|
||
if feature_count >= 0:
|
||
for fid in range(feature_count):
|
||
try:
|
||
feature = layer.GetFeature(fid)
|
||
|
||
except Exception as e:
|
||
geometry_read_errors += 1
|
||
attributes = get_dbf_attributes_direct(file_path, fid)
|
||
corrupted_features.append({"index": total + 1, "fid": fid, "shape": fid + 1, "stage": "GetFeature", "error": str(e), "attributes": attributes})
|
||
|
||
log.error(f"GetFeature, SHAPE : {Colors.ENDC}{fid + 1}{Colors.ERROR}, FID : {Colors.ENDC}{fid}{Colors.ERROR}, GetFeature : {Colors.ENDC}{e}" )
|
||
globalDat.errorCount += 1
|
||
log_feature_attributes(attributes, "error")
|
||
total += 1
|
||
continue
|
||
|
||
if feature is None:
|
||
geometry_read_errors += 1
|
||
corrupted_features.append({"fid": fid,"stage": "GetFeature","error": "GetFeature returned None"})
|
||
log.warning(f"Erreur lecture feature FID {Colors.ENDC}{fid}{Colors.WARNING} : GetFeature() retourne None")
|
||
continue
|
||
|
||
total += 1
|
||
geom = feature.GetGeometryRef()
|
||
|
||
if geom is None or geom.IsEmpty():
|
||
empty += 1
|
||
continue
|
||
|
||
geom_types[geom.GetGeometryName()] += 1
|
||
|
||
if not geom.IsValid():
|
||
invalid += 1
|
||
code, error_type, error_msg = get_geometry_error(geom)
|
||
error_codes[code] += 1
|
||
error_types[error_type] += 1
|
||
error_messages[error_msg] += 1
|
||
|
||
|
||
gtype = geom.GetGeometryType()
|
||
|
||
if ogr.GT_HasZ(gtype):
|
||
has_z = True
|
||
|
||
if ogr.GT_HasM(gtype):
|
||
has_m = True
|
||
|
||
# champs attributaires
|
||
layer_defn = layer.GetLayerDefn()
|
||
|
||
for i in range(layer_defn.GetFieldCount()):
|
||
field_name = layer_defn.GetFieldDefn(i).GetNameRef()
|
||
try:
|
||
val = feature.GetField(i)
|
||
except Exception as e:
|
||
log.error(f"Impossible d’exécuter GetField : {Colors.ENDC}{safe_relpath(file_path)}{Colors.ERROR} : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
continue
|
||
if val is not None:
|
||
field_stats[field_name].append(val)
|
||
|
||
|
||
|
||
elapsed = time.time() - start_time
|
||
file_size = os.path.getsize(file_path) / (1024*1024) # Mo
|
||
|
||
log.debug(f"Temps d'analyse : {Colors.ENDC}{elapsed:.2f}{Colors.DEBUG} s")
|
||
log.debug(f"Taille : {Colors.ENDC}{file_size:.2f}{Colors.DEBUG} Mo")
|
||
log.debug(f"Nombre d'objets : {Colors.ENDC}{total}")
|
||
|
||
if empty == 0 :
|
||
log.debug(f"Géométries vides : {Colors.ENDC}{empty}")
|
||
else :
|
||
log.warning(f"Géométries vides : {Colors.ENDC}{empty}")
|
||
|
||
if invalid == 0 :
|
||
log.info(f"Géométries invalides : {Colors.ENDC}{invalid}{Colors.INFO} sur {Colors.ENDC}{total}")
|
||
else :
|
||
log.warning(f"Géométries invalides : {Colors.ENDC}{invalid}{Colors.WARNING} sur {Colors.ENDC}{total}")
|
||
# for code, count in sorted( error_codes.items(), key=lambda item: item[1], reverse=True ):
|
||
# log.warning( f"\t\t{Colors.ENDC}{str(code):<20}{Colors.WARNING}: {Colors.ENDC}{count}" )
|
||
for error, count in sorted( error_types.items(), key=lambda item: item[1], reverse=True ):
|
||
log.warning( f"\t\t{Colors.ENDC}{error}{Colors.WARNING} : {Colors.ENDC}{count}" )
|
||
|
||
log.debug(f"MultiGeometries / Collections : {Colors.ENDC}{multi_geom_count}")
|
||
|
||
log.debug("Types géométriques :")
|
||
for gtype, count in geom_types.items():
|
||
log.debug(f"\t\t{gtype} : {Colors.ENDC}{count}")
|
||
|
||
log.debug("Bounding box :")
|
||
log.debug(f"\t\txmin = {Colors.ENDC}{extent[0]:>8.3f}{Colors.DEBUG}\txmax = {Colors.ENDC}{extent[1]:>8.3f}")
|
||
log.debug(f"\t\tymin = {Colors.ENDC}{extent[2]:>8.3f}{Colors.DEBUG}\tymax = {Colors.ENDC}{extent[3]:>8.3f}")
|
||
|
||
log.debug(f"CRS : {Colors.ENDC}{crs}")
|
||
|
||
log.debug(f"Dimensions, Z présent : {Colors.ENDC}{has_z}{Colors.DEBUG}\tM présent : {Colors.ENDC}{has_m}")
|
||
|
||
log.debug("Champs attributaires :")
|
||
|
||
for field, values in field_stats.items():
|
||
unique_count = len(set(values))
|
||
# log.debug(f"\t\t{Colors.ENDC}{field}{Colors.DEBUG}\t\t: {Colors.ENDC}{len(values)}{Colors.DEBUG} valeurs,\t\t{Colors.ENDC}{unique_count}{Colors.DEBUG} uniques")
|
||
# Définir une largeur fixe pour les noms de champs (par exemple 20 caractères)
|
||
field_width = 20
|
||
number_width = 10
|
||
|
||
log.debug(
|
||
f"\t\t{Colors.ENDC}{field:<{field_width}}{Colors.DEBUG}: "
|
||
f"{Colors.ENDC}{len(values):>{number_width}}{Colors.DEBUG} valeurs, "
|
||
f"{Colors.ENDC}{unique_count:>{number_width}}{Colors.DEBUG} uniques"
|
||
)
|
||
|
||
log.info(f"=========================================================================================================")
|
||
|
||
ds = None
|
||
|
||
return invalid, total
|
||
|
||
except Exception as e:
|
||
log.error(f"diagnostic file: {Colors.ENDC}{safe_relpath(file_path)}{Colors.ERROR}, unable to validate geometry: {Colors.ENDC}{e}{Colors.ERROR}, continuing anyway.")
|
||
globalDat.errorCount += 1
|
||
return -1, -1
|
||
|
||
#################################################################################################
|
||
def get_geometry_error(geom):
|
||
"""
|
||
Analyse la validité d'une géométrie OGR.
|
||
|
||
Retourne :
|
||
code : code d'erreur entier fourni par GDAL/OGR
|
||
error_type : type d'erreur normalisé
|
||
error_msg : message complet retourné par GEOS/OGR
|
||
"""
|
||
|
||
if geom is None:
|
||
return ( -1, "Null geometry", "Geometry is None" )
|
||
|
||
if geom.IsEmpty():
|
||
return ( -1, "Empty geometry", "Geometry is empty" )
|
||
|
||
# ------------------------------------------------------------------
|
||
# GEOMETRYCOLLECTION / MULTI*
|
||
# ------------------------------------------------------------------
|
||
|
||
geom_name = geom.GetGeometryName().upper()
|
||
|
||
if geom_name == "GEOMETRYCOLLECTION":
|
||
return -2, "Not supported", "Geometry not checked (GEOMETRYCOLLECTION)"
|
||
|
||
error_messages = []
|
||
|
||
def error_handler(err_class, err_no, message):
|
||
error_messages.append((err_no, message))
|
||
|
||
# Intercepte les messages GDAL/GEOS
|
||
gdal.PushErrorHandler(error_handler)
|
||
|
||
try:
|
||
valid = geom.IsValid()
|
||
|
||
finally:
|
||
gdal.PopErrorHandler()
|
||
|
||
# Géométrie valide
|
||
if valid:
|
||
return (
|
||
0,
|
||
"Valid",
|
||
"Geometry is valid"
|
||
)
|
||
|
||
# Récupération du code et du message GEOS
|
||
if error_messages:
|
||
code, error_msg = error_messages[0]
|
||
|
||
else:
|
||
code = -1
|
||
error_msg = "Invalid geometry"
|
||
|
||
# Normalisation du type d'erreur
|
||
msg = error_msg.lower()
|
||
|
||
if "ring self-intersection" in msg:
|
||
error_type = "Ring self-intersection"
|
||
|
||
elif "self-intersection" in msg:
|
||
error_type = "Self-intersection"
|
||
|
||
elif "hole lies outside shell" in msg:
|
||
error_type = "Hole lies outside shell"
|
||
|
||
elif "nested shells" in msg:
|
||
error_type = "Nested shells"
|
||
|
||
elif "too few points" in msg:
|
||
error_type = "Too few points"
|
||
|
||
elif "duplicate rings" in msg:
|
||
error_type = "Duplicate rings"
|
||
|
||
elif "invalid coordinate" in msg:
|
||
error_type = "Invalid coordinate"
|
||
|
||
else:
|
||
error_type = f"Unknown ({code})"
|
||
|
||
return code, error_type, error_msg
|
||
|
||
#################################################################################################
|
||
def fix_geometry(feature, infile, layer_defn):
|
||
|
||
try:
|
||
geom = feature.GetGeometryRef()
|
||
GetFID = feature.GetFID()
|
||
|
||
if geom is None:
|
||
return None
|
||
|
||
geom = geom.Clone()
|
||
|
||
geom.CloseRings() # ferme anneaux
|
||
|
||
# geom = geom.RemoveDuplicatePoints() # supprime points dupliqués
|
||
|
||
if not geom.IsValid(): # corrige topologie
|
||
geom_type = geom.GetGeometryName() if geom else "None"
|
||
|
||
code, error_type, error_msg = get_geometry_error(geom)
|
||
|
||
# Récupérer les attributs de l'objet
|
||
attrs = []
|
||
for i in range(layer_defn.GetFieldCount()):
|
||
field_name = layer_defn.GetFieldDefn(i).GetNameRef()
|
||
field_value = feature.GetField(i)
|
||
attrs.append(f"{Colors.ENDC}{field_name}{Colors.ERROR}={Colors.ENDC}{field_value}{Colors.ERROR}")
|
||
|
||
attrs_formatted = ', '.join(attrs)
|
||
|
||
if error_type == "Too few points" :
|
||
log.debug(
|
||
f"geometry in file : {Colors.ENDC}{infile}{Colors.DEBUG}, "
|
||
f"geometry type: {Colors.ENDC}{geom_type}{Colors.DEBUG}, "
|
||
f"FID: {Colors.ENDC}{GetFID}{Colors.DEBUG}, "
|
||
f"message code: {Colors.ENDC}{code}{Colors.DEBUG}, {Colors.ENDC}{error_msg}{Colors.DEBUG}, "
|
||
# f"attributes: {Colors.ENDC}{attrs_formatted}{Colors.DEBUG}"
|
||
)
|
||
|
||
elif error_type == "Not supported" :
|
||
log.debug(
|
||
f"geometry in file : {Colors.ENDC}{infile}{Colors.DEBUG}, "
|
||
f"geometry type: {Colors.ENDC}{geom_type}{Colors.DEBUG}, "
|
||
f"FID: {Colors.ENDC}{GetFID}{Colors.DEBUG}, "
|
||
f"message code: {Colors.ENDC}{code}{Colors.DEBUG}, {Colors.ENDC}{error_msg}{Colors.DEBUG}, "
|
||
# f"attributes: {Colors.ENDC}{attrs_formatted}{Colors.DEBUG}"
|
||
)
|
||
|
||
else :
|
||
error_info = (
|
||
f"invalid geometry in file : {Colors.ENDC}{infile}{Colors.ERROR}, "
|
||
f"geometry type: {Colors.ENDC}{geom_type}{Colors.ERROR}, "
|
||
f"FID: {Colors.ENDC}{GetFID}{Colors.ERROR}, "
|
||
f"message code: {Colors.ENDC}{code}{Colors.ERROR}, {Colors.ENDC}{error_msg}{Colors.ERROR}, "
|
||
f"attributes: {Colors.ENDC}{attrs_formatted}{Colors.ERROR}"
|
||
)
|
||
log.error(f"{error_info}")
|
||
globalDat.geometryErrors.append(error_info)
|
||
globalDat.errorCount += 1
|
||
|
||
geom = geom.MakeValid()
|
||
if not geom.IsValid():
|
||
return None
|
||
|
||
if geom is None or geom.IsEmpty(): # supprime géométries vides
|
||
log.warning(f"Empty geometry removed FID {Colors.ENDC}{GetFID}")
|
||
return None
|
||
|
||
gtype = geom.GetGeometryType()
|
||
|
||
if gtype in (ogr.wkbLineString, ogr.wkbLineString25D):
|
||
if geom.GetPointCount() < 2:
|
||
log.warning(f"Line geometry removed, insufficient number of points < 2 {Colors.ENDC}{GetFID}")
|
||
return None
|
||
|
||
if gtype == ogr.wkbPolygon:
|
||
ring = geom.GetGeometryRef(0)
|
||
if ring is None or ring.GetPointCount() < 4:
|
||
log.warning(f"Polygon geometry removed, insufficient number of points < 4 {Colors.ENDC}{GetFID}")
|
||
return None
|
||
|
||
return geom
|
||
|
||
except Exception as e:
|
||
log.error(f"Geometry in file {Colors.ENDC}{infile}{Colors.ERROR}, cannot be repaired : FID {Colors.ENDC}{GetFID}{Colors.ERROR}, code : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
return None
|
||
|
||
#################################################################################################
|
||
def shp2gpkgBad(pathshp, infile, outputspath, outfile):
|
||
"""
|
||
Conversion rapide SHP -> GPKG.
|
||
|
||
- support tous types de géométrie
|
||
- conserve Z et M
|
||
- ferme anneaux automatiquement
|
||
- corrige géométries invalides
|
||
- message uniquement si correction impossible
|
||
- optimisé gros fichiers
|
||
"""
|
||
|
||
input_shp = os.path.join(pathshp, infile + ".shp")
|
||
output_gpkg = os.path.join(outputspath, outfile + ".gpkg")
|
||
|
||
# geom_stats = defaultdict(int)
|
||
|
||
try:
|
||
if not gdal.GetConfigOption("GDAL_DATA"):
|
||
gdal.SetConfigOption("GDAL_DATA", "/usr/share/gdal")
|
||
|
||
gdal.SetConfigOption("OGR_GEOMETRY_ACCEPT_UNCLOSED_RING", "YES")
|
||
|
||
ds = ogr.Open(input_shp)
|
||
|
||
if ds is None:
|
||
log.error(f"shp2gpkg, impossible d'ouvrir le SHP : {Colors.ENDC}{safe_relpath(input_shp)}")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
layer = ds.GetLayer()
|
||
srs = layer.GetSpatialRef()
|
||
featureCount = layer.GetFeatureCount()
|
||
|
||
if os.path.exists(output_gpkg):
|
||
ogr.GetDriverByName("GPKG").DeleteDataSource(output_gpkg)
|
||
|
||
driver = ogr.GetDriverByName("GPKG")
|
||
out_ds = driver.CreateDataSource(output_gpkg)
|
||
|
||
out_layer = out_ds.CreateLayer(outfile, srs, geom_type=ogr.wkbUnknown)
|
||
layer_defn = layer.GetLayerDefn()
|
||
|
||
for i in range(layer_defn.GetFieldCount()):
|
||
out_layer.CreateField(layer_defn.GetFieldDefn(i))
|
||
|
||
out_layer_defn = out_layer.GetLayerDefn()
|
||
out_layer.StartTransaction()
|
||
|
||
error_count = 0
|
||
feature_count = 0
|
||
corrupted_features = []
|
||
total_count = len(layer)
|
||
total = 0
|
||
|
||
log.info(f"SHP file conversion : {Colors.ENDC}{infile}.shp{Colors.INFO} with {Colors.ENDC}{total_count}{Colors.INFO} objets")
|
||
|
||
with alive_bar(len(layer), title=f"{Colors.YELLOW}Conversion SHP file {Colors.ENDC}{infile}{Colors.YELLOW} to GPKG {Colors.ENDC}" , length = 20) as bar:
|
||
if featureCount >= 0:
|
||
for fid in range(featureCount):
|
||
# ----------------------------------------------------------
|
||
# Lecture du feature
|
||
# ----------------------------------------------------------
|
||
try:
|
||
feature = layer.GetFeature(fid)
|
||
|
||
except Exception as e:
|
||
error_count += 1
|
||
attributes = get_dbf_attributes_direct(input_shp, fid)
|
||
corrupted_features.append({"index": total + 1, "fid": fid, "shape": fid + 1, "stage": "GetFeature", "error": str(e), "attributes": attributes})
|
||
|
||
log.error(f"GetFeature, SHAPE : {Colors.ENDC}{fid + 1}{Colors.ERROR}, FID : {Colors.ENDC}{fid}{Colors.ERROR}, GetFeature : {Colors.ENDC}{e}" )
|
||
globalDat.errorCount += 1
|
||
log_feature_attributes(attributes, "error")
|
||
total += 1
|
||
bar()
|
||
continue
|
||
|
||
geom = fix_geometry(feature, infile, layer_defn)
|
||
|
||
if geom is None :
|
||
log.warning(f"Géométrie impossible à corriger FID")
|
||
error_count += 1
|
||
corrupted_features.append({
|
||
"index": total + 1,
|
||
"fid": fid,
|
||
"shape": fid + 1,
|
||
"stage": "fix_geometry",
|
||
"error": "Geometry is None",
|
||
"attributes": get_dbf_attributes_direct(input_shp, fid)
|
||
})
|
||
|
||
total += 1
|
||
bar()
|
||
continue
|
||
|
||
# geom_type_name = geom.GetGeometryName()
|
||
# geom_stats[geom_type_name] += 1
|
||
|
||
# création feature
|
||
out_feature = ogr.Feature(out_layer_defn)
|
||
|
||
# copie attributs
|
||
for i in range(out_layer_defn.GetFieldCount()):
|
||
out_feature.SetField(i, feature.GetField(i))
|
||
|
||
out_feature.SetGeometry(geom)
|
||
out_layer.CreateFeature(out_feature)
|
||
out_feature = None
|
||
feature_count += 1
|
||
|
||
# commit par bloc (performance)
|
||
if feature_count % 10000 == 0:
|
||
out_layer.CommitTransaction()
|
||
out_layer.StartTransaction()
|
||
|
||
bar()
|
||
|
||
out_layer.CommitTransaction()
|
||
|
||
ds = None
|
||
out_ds = None
|
||
|
||
# total = 0
|
||
log.info(f"Conversion GPKG terminée fichier: {Colors.ENDC}{outfile}{Colors.INFO}, {Colors.ENDC}{feature_count}{Colors.INFO} objets convertis")
|
||
# for gtype, count in sorted(geom_stats.items()):
|
||
# log.info(f"Type : {gtype} -> {Colors.ENDC}{count}")
|
||
# total += count
|
||
# log.info(f"Total -> {Colors.ENDC}{total}")
|
||
|
||
|
||
if error_count > 0: log.warning(f"{Colors.ENDC}{error_count}{Colors.WARNING} géométries n'ont pas pu être corrigées")
|
||
|
||
if (total_count - feature_count) > 0 : log.warning(f"{Colors.ENDC}{total_count - feature_count}{Colors.WARNING} géométries supprimées")
|
||
|
||
except RuntimeError as e:
|
||
|
||
if log:
|
||
log.error(f"Error in conversion file {Colors.ENDC}{infile}{Colors.ERROR} SHP to GPKG : {Colors.ENDC}{e}{Colors.ERROR}")
|
||
globalDat.errorCount += 1
|
||
|
||
return
|
||
|
||
|
||
# except Exception as e:
|
||
|
||
# if log:
|
||
# log.error(f"Error in conversion file {infile} SHP to GPKG : {e}")
|
||
# globalDat.errorCount += 1
|
||
|
||
# raise
|
||
#################################################################################################
|
||
def shp2gpkg(pathshp, infile, outputspath, outfile):
|
||
"""
|
||
Conversion SHP -> GPKG.
|
||
|
||
- supporte tous les types de géométrie
|
||
- conserve Z et M
|
||
- ferme les anneaux automatiquement via fix_geometry()
|
||
- corrige les géométries invalides via fix_geometry()
|
||
- ne crée JAMAIS de feature avec une géométrie None
|
||
- détecte les erreurs de lecture
|
||
- détecte les erreurs de création dans le GPKG
|
||
- ne suppose PAS que les FID sont continus
|
||
- optimisé pour les gros fichiers
|
||
"""
|
||
|
||
input_shp = os.path.join(pathshp, infile + ".shp")
|
||
output_gpkg = os.path.join(outputspath, outfile + ".gpkg")
|
||
ds = None
|
||
out_ds = None
|
||
out_layer = None
|
||
error_count = 0
|
||
feature_count = 0
|
||
processed_count = 0
|
||
corrupted_features = []
|
||
|
||
try:
|
||
if not gdal.GetConfigOption("GDAL_DATA"):
|
||
gdal.SetConfigOption("GDAL_DATA", "/usr/share/gdal")
|
||
|
||
gdal.SetConfigOption("OGR_GEOMETRY_ACCEPT_UNCLOSED_RING", "YES")
|
||
|
||
if not os.path.exists(input_shp):
|
||
log.error(f"shp2gpkg, fichier SHP inexistant : {Colors.ENDC}{safe_relpath(input_shp)}")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
ds = ogr.Open(input_shp)
|
||
|
||
if ds is None:
|
||
log.error(f"shp2gpkg, impossible d'ouvrir le SHP : {Colors.ENDC}{safe_relpath(input_shp)}")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
layer = ds.GetLayer()
|
||
|
||
if layer is None:
|
||
log.error(f"shp2gpkg, impossible de récupérer la couche : {Colors.ENDC}{safe_relpath(input_shp)}")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
srs = layer.GetSpatialRef()
|
||
featureCount = layer.GetFeatureCount()
|
||
|
||
if featureCount < 0:
|
||
log.error(f"shp2gpkg, impossible de déterminer le nombre de features : {Colors.ENDC}{safe_relpath(input_shp)}")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
layer_defn = layer.GetLayerDefn()
|
||
|
||
if layer_defn is None:
|
||
log.error(f"shp2gpkg, impossible de récupérer la définition de la couche : {Colors.ENDC}{safe_relpath(input_shp)}")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
total_count = featureCount
|
||
|
||
log.info(f"SHP file conversion : {Colors.ENDC}{infile}.shp{Colors.INFO} with {Colors.ENDC}{total_count}{Colors.INFO} objets")
|
||
|
||
if os.path.exists(output_gpkg):
|
||
try:
|
||
ogr.GetDriverByName("GPKG").DeleteDataSource(output_gpkg)
|
||
|
||
except Exception as e:
|
||
log.error(f"Impossible de supprimer le GPKG existant : {Colors.ENDC}{output_gpkg}{Colors.ERROR} : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
driver = ogr.GetDriverByName("GPKG")
|
||
|
||
if driver is None:
|
||
log.error("shp2gpkg, driver GPKG GDAL indisponible")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
out_ds = driver.CreateDataSource(output_gpkg)
|
||
|
||
if out_ds is None:
|
||
log.error(f"shp2gpkg, impossible de créer le GPKG : {Colors.ENDC}{output_gpkg}")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
out_layer = out_ds.CreateLayer(outfile, srs, geom_type=ogr.wkbUnknown )
|
||
|
||
if out_layer is None:
|
||
log.error(f"shp2gpkg, impossible de créer la couche GPKG : {Colors.ENDC}{outfile}")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
for i in range(layer_defn.GetFieldCount()):
|
||
|
||
field_defn = layer_defn.GetFieldDefn(i)
|
||
if field_defn is None:
|
||
log.warning(f"Définition de champ invalide index {Colors.ENDC}{i}")
|
||
continue
|
||
|
||
result = out_layer.CreateField(field_defn)
|
||
|
||
if result != 0:
|
||
log.error(f"Impossible de créer le champ {Colors.ENDC}{field_defn.GetNameRef()}")
|
||
globalDat.errorCount += 1
|
||
|
||
out_layer_defn = out_layer.GetLayerDefn()
|
||
|
||
if out_layer_defn is None:
|
||
log.error("shp2gpkg, impossible de récupérer la définition de la couche de sortie")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
out_layer.StartTransaction()
|
||
|
||
# ============================================================
|
||
# Parcours réel des features
|
||
#
|
||
# Ne PAS utiliser :
|
||
#
|
||
# for fid in range(featureCount)
|
||
# feature = layer.GetFeature(fid)
|
||
#
|
||
# Les FID d'un SHP peuvent ne pas être continus.
|
||
# ============================================================
|
||
|
||
layer.ResetReading()
|
||
|
||
with alive_bar(total_count, title=(f"{Colors.YELLOW}Conversion SHP file {Colors.ENDC}{infile}{Colors.YELLOW} to GPKG {Colors.ENDC}"),length=20) as bar:
|
||
|
||
while True:
|
||
|
||
# ====================================================
|
||
# Lecture du prochain feature
|
||
# ====================================================
|
||
|
||
try:
|
||
feature = layer.GetNextFeature()
|
||
|
||
except Exception as e:
|
||
error_count += 1
|
||
|
||
log.error(f"Erreur lecture SHP après {Colors.ENDC}{processed_count}{Colors.ERROR} features : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
|
||
bar()
|
||
continue
|
||
|
||
# ====================================================
|
||
# Fin normale de la couche
|
||
# ====================================================
|
||
|
||
if feature is None:
|
||
break
|
||
|
||
# ====================================================
|
||
# Récupération du vrai FID
|
||
# ====================================================
|
||
|
||
try:
|
||
fid = feature.GetFID()
|
||
|
||
except Exception:
|
||
fid = processed_count
|
||
|
||
processed_count += 1
|
||
|
||
# ====================================================
|
||
# Vérification de la géométrie brute
|
||
# ====================================================
|
||
|
||
try:
|
||
source_geom = feature.GetGeometryRef()
|
||
|
||
except Exception as e:
|
||
error_count += 1
|
||
|
||
try:
|
||
attributes = get_dbf_attributes_direct(input_shp, fid)
|
||
|
||
except Exception:
|
||
attributes = None
|
||
|
||
corrupted_features.append({
|
||
"index": processed_count,
|
||
"fid": fid,
|
||
"shape": processed_count,
|
||
"stage": "GetGeometryRef",
|
||
"error": str(e),
|
||
"attributes": attributes
|
||
})
|
||
|
||
log.error(f"GetGeometryRef, SHAPE : {Colors.ENDC}{processed_count}{Colors.ERROR}, FID : {Colors.ENDC}{fid}{Colors.ERROR} : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
|
||
if attributes is not None:
|
||
log_feature_attributes(attributes, "error")
|
||
|
||
bar()
|
||
continue
|
||
|
||
# ====================================================
|
||
# Géométrie NULL
|
||
# ====================================================
|
||
|
||
if source_geom is None:
|
||
error_count += 1
|
||
|
||
try:
|
||
attributes = get_dbf_attributes_direct(input_shp, fid)
|
||
|
||
except Exception:
|
||
attributes = None
|
||
|
||
corrupted_features.append({
|
||
"index": processed_count,
|
||
"fid": fid,
|
||
"shape": processed_count,
|
||
"stage": "GetGeometryRef",
|
||
"error": "Source geometry is None",
|
||
"attributes": attributes
|
||
})
|
||
|
||
log.error(f"Géométrie NULL, SHAPE : {Colors.ENDC}{processed_count}{Colors.ERROR}, FID : {Colors.ENDC}{fid}")
|
||
globalDat.errorCount += 1
|
||
|
||
if attributes is not None:
|
||
log_feature_attributes(attributes, "error")
|
||
|
||
bar()
|
||
continue
|
||
|
||
# ====================================================
|
||
# Correction de la géométrie
|
||
# ====================================================
|
||
|
||
try:
|
||
geom = fix_geometry(feature, infile, layer_defn)
|
||
|
||
except Exception as e:
|
||
geom = None
|
||
error_count += 1
|
||
|
||
try:
|
||
attributes = get_dbf_attributes_direct(input_shp, fid)
|
||
|
||
except Exception:
|
||
attributes = None
|
||
|
||
corrupted_features.append({
|
||
"index": processed_count,
|
||
"fid": fid,
|
||
"shape": processed_count,
|
||
"stage": "fix_geometry",
|
||
"error": str(e),
|
||
"attributes": attributes
|
||
})
|
||
|
||
log.error(f"Erreur fix_geometry, SHAPE : {Colors.ENDC}{processed_count}{Colors.ERROR}, FID : {Colors.ENDC}{fid}{Colors.ERROR} : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
|
||
if attributes is not None:
|
||
log_feature_attributes(attributes, "error")
|
||
|
||
bar()
|
||
continue
|
||
|
||
# ====================================================
|
||
# Géométrie impossible à corriger
|
||
# ====================================================
|
||
|
||
if geom is None:
|
||
error_count += 1
|
||
|
||
try:
|
||
attributes = get_dbf_attributes_direct(input_shp, fid)
|
||
|
||
except Exception:
|
||
attributes = None
|
||
|
||
corrupted_features.append({
|
||
"index": processed_count,
|
||
"fid": fid,
|
||
"shape": processed_count,
|
||
"stage": "fix_geometry",
|
||
"error": "fix_geometry returned None",
|
||
"attributes": attributes
|
||
})
|
||
|
||
log.warning(f"Géométrie impossible à corriger, SHAPE : {Colors.ENDC}{processed_count}{Colors.WARNING}, FID : {Colors.ENDC}{fid}")
|
||
globalDat.errorCount += 1
|
||
|
||
if attributes is not None:
|
||
log_feature_attributes(attributes, "error")
|
||
|
||
bar()
|
||
continue
|
||
|
||
# ====================================================
|
||
# Vérification finale de la géométrie
|
||
# ====================================================
|
||
|
||
try:
|
||
if geom.IsEmpty():
|
||
error_count += 1
|
||
|
||
log.warning(f"Géométrie vide après correction, SHAPE : {Colors.ENDC}{processed_count}{Colors.WARNING}, FID : {Colors.ENDC}{fid}")
|
||
|
||
corrupted_features.append({
|
||
"index": processed_count,
|
||
"fid": fid,
|
||
"shape": processed_count,
|
||
"stage": "geometry_validation",
|
||
"error": "Geometry is empty",
|
||
"attributes": None
|
||
})
|
||
|
||
globalDat.errorCount += 1
|
||
bar()
|
||
continue
|
||
|
||
except Exception as e:
|
||
error_count += 1
|
||
|
||
log.warning(f"Impossible de vérifier la géométrie SHAPE {Colors.ENDC}{processed_count}{Colors.WARNING} : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
bar()
|
||
continue
|
||
|
||
# ====================================================
|
||
# Création feature
|
||
# ====================================================
|
||
|
||
out_feature = ogr.Feature(out_layer_defn)
|
||
|
||
if out_feature is None:
|
||
error_count += 1
|
||
|
||
log.error(f"Impossible de créer le feature GPKG, SHAPE : {Colors.ENDC}{processed_count}")
|
||
globalDat.errorCount += 1
|
||
bar()
|
||
continue
|
||
|
||
# ====================================================
|
||
# Copie attributs
|
||
# ====================================================
|
||
|
||
try:
|
||
|
||
for i in range(out_layer_defn.GetFieldCount()):
|
||
out_feature.SetField(i, feature.GetField(i))
|
||
|
||
except Exception as e:
|
||
error_count += 1
|
||
|
||
log.error(f"Erreur copie attributs, SHAPE : {Colors.ENDC}{processed_count}{Colors.ERROR} : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
|
||
out_feature = None
|
||
bar()
|
||
continue
|
||
|
||
# ====================================================
|
||
# Affectation géométrie
|
||
# ====================================================
|
||
|
||
try:
|
||
out_feature.SetGeometry(geom)
|
||
|
||
except Exception as e:
|
||
error_count += 1
|
||
|
||
log.error(f"Erreur SetGeometry, SHAPE : {Colors.ENDC}{processed_count}{Colors.ERROR} : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
|
||
out_feature = None
|
||
bar()
|
||
continue
|
||
|
||
# ====================================================
|
||
# Création dans le GPKG
|
||
# ====================================================
|
||
|
||
try:
|
||
result = out_layer.CreateFeature(out_feature)
|
||
|
||
except Exception as e:
|
||
result = 1
|
||
|
||
log.error(f"Erreur CreateFeature, SHAPE : {Colors.ENDC}{processed_count}{Colors.ERROR}, FID : {Colors.ENDC}{fid}{Colors.ERROR} : {Colors.ENDC}{e}")
|
||
|
||
finally:
|
||
out_feature = None
|
||
|
||
if result != 0:
|
||
error_count += 1
|
||
|
||
log.error(f"Impossible de créer le feature GPKG, SHAPE : {Colors.ENDC}{processed_count}{Colors.ERROR}, FID : {Colors.ENDC}{fid}")
|
||
globalDat.errorCount += 1
|
||
|
||
bar()
|
||
continue
|
||
|
||
feature_count += 1
|
||
|
||
# ====================================================
|
||
# Commit par bloc
|
||
# ====================================================
|
||
|
||
if feature_count % 10000 == 0:
|
||
|
||
try:
|
||
out_layer.CommitTransaction()
|
||
out_layer.StartTransaction()
|
||
|
||
except Exception as e:
|
||
log.error(f"Erreur transaction GPKG après {Colors.ENDC}{feature_count}{Colors.ERROR} features : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
raise
|
||
|
||
bar()
|
||
|
||
# ============================================================
|
||
# Commit final
|
||
# ============================================================
|
||
|
||
try:
|
||
out_layer.CommitTransaction()
|
||
|
||
except Exception as e:
|
||
log.error(f"Erreur CommitTransaction final : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
raise
|
||
|
||
layer = None
|
||
ds = None
|
||
|
||
out_layer = None
|
||
out_ds = None
|
||
|
||
log.info(f"Conversion GPKG terminée fichier : {Colors.ENDC}{outfile}{Colors.INFO}, {Colors.ENDC}{feature_count}{Colors.INFO} objets convertis")
|
||
|
||
if error_count > 0:
|
||
log.warning(f"{Colors.ENDC}{error_count}{Colors.WARNING} objets en erreur")
|
||
|
||
deleted_count = total_count - feature_count
|
||
|
||
if deleted_count > 0:
|
||
log.warning(f"{Colors.ENDC}{deleted_count}{Colors.WARNING} géométries supprimées")
|
||
|
||
if corrupted_features:
|
||
log.warning(f"{Colors.ENDC}{len(corrupted_features)}{Colors.WARNING} features rejetés pendant la conversion")
|
||
|
||
for item in corrupted_features:
|
||
log.warning(f"SHAPE {Colors.ENDC}{item['shape']}{Colors.WARNING} / FID {Colors.ENDC}{item['fid']}{Colors.WARNING} / {Colors.ENDC}{item['stage']}{Colors.WARNING} : {Colors.ENDC}{item['error']}")
|
||
|
||
return
|
||
|
||
# =================================================================
|
||
# Erreur GDAL / Runtime
|
||
# =================================================================
|
||
|
||
except RuntimeError as e:
|
||
|
||
log.error(f"Error in conversion file {Colors.ENDC}{infile}{Colors.ERROR} SHP to GPKG : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
|
||
# =================================================================
|
||
# Erreur inattendue
|
||
# =================================================================
|
||
|
||
except Exception as e:
|
||
|
||
log.error(f"Erreur inattendue dans shp2gpkg {Colors.ENDC}{infile}{Colors.ERROR} : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
|
||
# =================================================================
|
||
# Nettoyage garanti
|
||
# =================================================================
|
||
|
||
finally:
|
||
|
||
try:
|
||
if out_layer is not None: out_layer = None
|
||
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
if out_ds is not None: out_ds = None
|
||
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
if ds is not None: ds = None
|
||
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
|
||
|
||
def shp2gpkg2(pathshp, infile, outputspath, outfile):
|
||
"""
|
||
Conversion SHP -> GPKG.
|
||
|
||
- supporte tous les types de géométrie
|
||
- conserve Z et M
|
||
- ferme les anneaux automatiquement via fix_geometry()
|
||
- corrige les géométries invalides via fix_geometry()
|
||
- ne crée JAMAIS de feature avec une géométrie None
|
||
- détecte GetFeature() retournant None
|
||
- détecte les erreurs de création dans le GPKG
|
||
- optimisé pour les gros fichiers
|
||
"""
|
||
|
||
input_shp = os.path.join(pathshp, infile + ".shp")
|
||
output_gpkg = os.path.join(outputspath, outfile + ".gpkg")
|
||
ds = None
|
||
out_ds = None
|
||
out_layer = None
|
||
error_count = 0
|
||
feature_count = 0
|
||
corrupted_features = []
|
||
|
||
try:
|
||
if not gdal.GetConfigOption("GDAL_DATA"):
|
||
gdal.SetConfigOption("GDAL_DATA", "/usr/share/gdal")
|
||
|
||
gdal.SetConfigOption("OGR_GEOMETRY_ACCEPT_UNCLOSED_RING", "YES")
|
||
|
||
if not os.path.exists(input_shp):
|
||
log.error(f"shp2gpkg, fichier SHP inexistant : {Colors.ENDC}{safe_relpath(input_shp)}")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
ds = ogr.Open(input_shp)
|
||
|
||
if ds is None:
|
||
log.error(f"shp2gpkg, impossible d'ouvrir le SHP : {Colors.ENDC}{safe_relpath(input_shp)}")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
layer = ds.GetLayer()
|
||
|
||
if layer is None:
|
||
log.error(f"shp2gpkg, impossible de récupérer la couche : {Colors.ENDC}{safe_relpath(input_shp)}")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
srs = layer.GetSpatialRef()
|
||
featureCount = layer.GetFeatureCount()
|
||
|
||
if featureCount < 0:
|
||
log.error(f"shp2gpkg, impossible de déterminer le nombre de features : {Colors.ENDC}{safe_relpath(input_shp)}")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
layer_defn = layer.GetLayerDefn()
|
||
|
||
if layer_defn is None:
|
||
log.error(f"shp2gpkg, impossible de récupérer la définition de la couche : {Colors.ENDC}{safe_relpath(input_shp)}")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
total_count = featureCount
|
||
|
||
log.info(f"SHP file conversion : {Colors.ENDC}{infile}.shp{Colors.INFO} with {Colors.ENDC}{total_count}{Colors.INFO} objets")
|
||
|
||
if os.path.exists(output_gpkg):
|
||
try:
|
||
ogr.GetDriverByName("GPKG").DeleteDataSource(output_gpkg)
|
||
|
||
except Exception as e:
|
||
log.error(f"Impossible de supprimer le GPKG existant : {Colors.ENDC}{output_gpkg}{Colors.ERROR} : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
return
|
||
driver = ogr.GetDriverByName("GPKG")
|
||
|
||
if driver is None:
|
||
log.error("shp2gpkg, driver GPKG GDAL indisponible")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
out_ds = driver.CreateDataSource(output_gpkg)
|
||
|
||
if out_ds is None:
|
||
log.error(f"shp2gpkg, impossible de créer le GPKG : {Colors.ENDC}{output_gpkg}")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
out_layer = out_ds.CreateLayer(outfile, srs, geom_type=ogr.wkbUnknown )
|
||
|
||
if out_layer is None:
|
||
log.error(f"shp2gpkg, impossible de créer la couche GPKG : {Colors.ENDC}{outfile}")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
for i in range(layer_defn.GetFieldCount()):
|
||
|
||
field_defn = layer_defn.GetFieldDefn(i)
|
||
if field_defn is None:
|
||
log.warning(f"Définition de champ invalide index {Colors.ENDC}{i}")
|
||
continue
|
||
|
||
result = out_layer.CreateField(field_defn)
|
||
|
||
if result != 0:
|
||
log.error(f"Impossible de créer le champ {Colors.ENDC}{field_defn.GetNameRef()}")
|
||
globalDat.errorCount += 1
|
||
|
||
out_layer_defn = out_layer.GetLayerDefn()
|
||
|
||
if out_layer_defn is None:
|
||
log.error("shp2gpkg, impossible de récupérer la définition de la couche de sortie")
|
||
globalDat.errorCount += 1
|
||
return
|
||
|
||
out_layer.StartTransaction()
|
||
|
||
|
||
with alive_bar(total_count, title=(f"{Colors.YELLOW}Conversion SHP file {Colors.ENDC}{infile}{Colors.YELLOW} to GPKG {Colors.ENDC}"),length=20) as bar:
|
||
for fid in range(featureCount):
|
||
try:
|
||
feature = layer.GetFeature(fid)
|
||
|
||
except Exception as e:
|
||
error_count += 1
|
||
attributes = get_dbf_attributes_direct(input_shp, fid)
|
||
corrupted_features.append({
|
||
"index": fid + 1,
|
||
"fid": fid,
|
||
"shape": fid + 1,
|
||
"stage": "GetFeature",
|
||
"error": str(e),
|
||
"attributes": attributes
|
||
})
|
||
|
||
log.error(f"GetFeature, SHAPE : {Colors.ENDC}{fid + 1}{Colors.ERROR}, FID : {Colors.ENDC}{fid}{Colors.ERROR}, GetFeature :{Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
log_feature_attributes(attributes, "error")
|
||
bar()
|
||
continue
|
||
|
||
if feature is None:
|
||
error_count += 1
|
||
try:
|
||
attributes = get_dbf_attributes_direct(input_shp, fid)
|
||
|
||
except Exception:
|
||
attributes = None
|
||
|
||
corrupted_features.append({
|
||
"index": fid + 1,
|
||
"fid": fid,
|
||
"shape": fid + 1,
|
||
"stage": "GetFeature",
|
||
"error": "GetFeature returned None",
|
||
"attributes": attributes
|
||
})
|
||
|
||
log.error(f"GetFeature retourne None, SHAPE : {Colors.ENDC}{fid + 1}{Colors.ERROR}, FID : {Colors.ENDC}{fid}")
|
||
globalDat.errorCount += 1
|
||
|
||
if attributes is not None:
|
||
log_feature_attributes(attributes, "error")
|
||
|
||
bar()
|
||
continue
|
||
|
||
# ====================================================
|
||
# Vérification de la géométrie brute
|
||
# ====================================================
|
||
|
||
try:
|
||
source_geom = feature.GetGeometryRef()
|
||
|
||
except Exception as e:
|
||
error_count += 1
|
||
|
||
try:
|
||
attributes = get_dbf_attributes_direct( input_shp, fid)
|
||
|
||
except Exception:
|
||
attributes = None
|
||
|
||
corrupted_features.append({
|
||
"index": fid + 1,
|
||
"fid": fid,
|
||
"shape": fid + 1,
|
||
"stage": "GetGeometryRef",
|
||
"error": str(e),
|
||
"attributes": attributes
|
||
})
|
||
|
||
log.error(f"GetGeometryRef, SHAPE : {Colors.ENDC}{fid + 1}{Colors.ERROR}, FID : {Colors.ENDC}{fid}{Colors.ERROR} : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
|
||
if attributes is not None:
|
||
log_feature_attributes(attributes, "error")
|
||
|
||
bar()
|
||
continue
|
||
|
||
# ====================================================
|
||
# Géométrie NULL
|
||
# ====================================================
|
||
|
||
if source_geom is None:
|
||
error_count += 1
|
||
|
||
try:
|
||
attributes = get_dbf_attributes_direct(input_shp, fid)
|
||
|
||
except Exception:
|
||
attributes = None
|
||
|
||
corrupted_features.append({
|
||
"index": fid + 1,
|
||
"fid": fid,
|
||
"shape": fid + 1,
|
||
"stage": "GetGeometryRef",
|
||
"error": "Source geometry is None",
|
||
"attributes": attributes
|
||
})
|
||
|
||
log.error(f"Géométrie NULL, SHAPE : {Colors.ENDC}{fid + 1}{Colors.ERROR}, FID : {Colors.ENDC}{fid}")
|
||
globalDat.errorCount += 1
|
||
if attributes is not None:
|
||
log_feature_attributes(attributes, "error")
|
||
|
||
bar()
|
||
continue
|
||
|
||
# ====================================================
|
||
# Correction de la géométrie
|
||
# ====================================================
|
||
|
||
try:
|
||
|
||
geom = fix_geometry(feature, infile, layer_defn)
|
||
|
||
except Exception as e:
|
||
geom = None
|
||
error_count += 1
|
||
try:
|
||
attributes = get_dbf_attributes_direct(input_shp, fid)
|
||
|
||
except Exception:
|
||
attributes = None
|
||
|
||
corrupted_features.append({
|
||
"index": fid + 1,
|
||
"fid": fid,
|
||
"shape": fid + 1,
|
||
"stage": "fix_geometry",
|
||
"error": str(e),
|
||
"attributes": attributes
|
||
})
|
||
|
||
log.error(f"Erreur fix_geometry, SHAPE : {Colors.ENDC}{fid + 1}{Colors.ERROR}, FID : {Colors.ENDC}{fid}{Colors.ERROR} : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
if attributes is not None:
|
||
log_feature_attributes(attributes, "error")
|
||
|
||
bar()
|
||
continue
|
||
|
||
if geom is None:
|
||
error_count += 1
|
||
|
||
try:
|
||
attributes = get_dbf_attributes_direct(input_shp, fid)
|
||
|
||
except Exception:
|
||
attributes = None
|
||
|
||
corrupted_features.append({
|
||
"index": fid + 1,
|
||
"fid": fid,
|
||
"shape": fid + 1,
|
||
"stage": "fix_geometry",
|
||
"error": "fix_geometry returned None",
|
||
"attributes": attributes
|
||
})
|
||
|
||
log.warning(f"Géométrie impossible à corriger, SHAPE : {Colors.ENDC}{fid + 1}{Colors.WARNING}, FID : {Colors.ENDC}{fid}")
|
||
globalDat.errorCount += 1
|
||
if attributes is not None:
|
||
log_feature_attributes(attributes, "error")
|
||
|
||
bar()
|
||
continue
|
||
|
||
# ====================================================
|
||
# Vérification finale de la géométrie
|
||
# ====================================================
|
||
|
||
try:
|
||
if geom.IsEmpty():
|
||
error_count += 1
|
||
log.warning(f"Géométrie vide après correction, SHAPE : {Colors.ENDC}{fid + 1}{Colors.WARNING}, FID : {Colors.ENDC}{fid}")
|
||
|
||
corrupted_features.append({
|
||
"index": fid + 1,
|
||
"fid": fid,
|
||
"shape": fid + 1,
|
||
"stage": "geometry_validation",
|
||
"error": "Geometry is empty",
|
||
"attributes": None
|
||
})
|
||
|
||
globalDat.errorCount += 1
|
||
bar()
|
||
continue
|
||
|
||
except Exception as e:
|
||
error_count += 1
|
||
log.warning(f"Impossible de vérifier la géométrie SHAPE {Colors.ENDC}{fid + 1}{Colors.WARNING} : {Colors.ENDC}{e}")
|
||
bar()
|
||
continue
|
||
|
||
out_feature = ogr.Feature(out_layer_defn)
|
||
|
||
if out_feature is None:
|
||
error_count += 1
|
||
log.error(f"Impossible de créer le feature GPKG, SHAPE : {Colors.ENDC}{fid + 1}")
|
||
globalDat.errorCount += 1
|
||
bar()
|
||
continue
|
||
|
||
try:
|
||
|
||
for i in range(out_layer_defn.GetFieldCount()):
|
||
out_feature.SetField(i, feature.GetField(i))
|
||
|
||
except Exception as e:
|
||
error_count += 1
|
||
log.error(f"Erreur copie attributs, SHAPE : {Colors.ENDC}{fid + 1}{Colors.ERROR} : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
out_feature = None
|
||
bar()
|
||
continue
|
||
|
||
try:
|
||
out_feature.SetGeometry(geom)
|
||
|
||
except Exception as e:
|
||
error_count += 1
|
||
log.error(f"Erreur SetGeometry, SHAPE : {Colors.ENDC}{fid + 1}{Colors.ERROR} : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
out_feature = None
|
||
bar()
|
||
continue
|
||
|
||
try:
|
||
result = out_layer.CreateFeature(out_feature)
|
||
|
||
except Exception as e:
|
||
result = 1
|
||
log.error(f"Erreur CreateFeature, SHAPE : {Colors.ENDC}{fid + 1}{Colors.ERROR} : {Colors.ENDC}{e}")
|
||
|
||
finally:
|
||
out_feature = None
|
||
|
||
if result != 0:
|
||
error_count += 1
|
||
log.error(f"Impossible de créer le feature GPKG, SHAPE : {Colors.ENDC}{fid + 1}{Colors.ERROR}, FID : {Colors.ENDC}{fid}")
|
||
globalDat.errorCount += 1
|
||
bar()
|
||
continue
|
||
|
||
feature_count += 1
|
||
|
||
if feature_count % 10000 == 0:
|
||
|
||
try:
|
||
out_layer.CommitTransaction()
|
||
out_layer.StartTransaction()
|
||
|
||
except Exception as e:
|
||
log.error(f"Erreur transaction GPKG après {Colors.ENDC}{feature_count}{Colors.ERROR} features : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
raise
|
||
bar()
|
||
|
||
try:
|
||
out_layer.CommitTransaction()
|
||
|
||
except Exception as e:
|
||
|
||
log.error(f"Erreur CommitTransaction final : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
raise
|
||
|
||
|
||
layer = None
|
||
ds = None
|
||
|
||
out_layer = None
|
||
out_ds = None
|
||
|
||
log.info(f"Conversion GPKG terminée fichier : {Colors.ENDC}{outfile}{Colors.INFO}, {Colors.ENDC}{feature_count}{Colors.INFO} objets convertis")
|
||
|
||
if error_count > 0:
|
||
log.warning(f"{Colors.ENDC}{error_count}{Colors.WARNING} objets en erreur")
|
||
|
||
deleted_count = total_count - feature_count
|
||
|
||
if deleted_count > 0:
|
||
log.warning(f"{Colors.ENDC}{deleted_count}{Colors.WARNING} géométries supprimées")
|
||
|
||
if corrupted_features:
|
||
log.warning(f"{Colors.ENDC}{len(corrupted_features)}{Colors.WARNING} features rejetés pendant la conversion")
|
||
for item in corrupted_features:
|
||
log.warning(f"SHAPE {Colors.ENDC}{item['shape']}{Colors.WARNING} / FID {Colors.ENDC}{item['fid']}{Colors.WARNING} / {Colors.ENDC}{item['stage']}{Colors.WARNING} : {Colors.ENDC}{item['error']}")
|
||
return
|
||
|
||
# =================================================================
|
||
# Erreur GDAL / Runtime
|
||
# =================================================================
|
||
except RuntimeError as e:
|
||
log.error(
|
||
f"Error in conversion file {Colors.ENDC}{infile}{Colors.ERROR} SHP to GPKG : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
|
||
# =================================================================
|
||
# Erreur inattendue
|
||
# =================================================================
|
||
except Exception as e:
|
||
log.error(f"Erreur inattendue dans shp2gpkg {Colors.ENDC}{infile}{Colors.ERROR} : {Colors.ENDC}{e}")
|
||
globalDat.errorCount += 1
|
||
|
||
# =================================================================
|
||
# Nettoyage garanti
|
||
# =================================================================
|
||
|
||
finally:
|
||
try:
|
||
if out_layer is not None: out_layer = None
|
||
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
if out_ds is not None: out_ds = None
|
||
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
if ds is not None: ds = None
|
||
|
||
except Exception:
|
||
pass
|
||
|
||
#################################################################################################
|
||
def count_topology_errors(file_path):
|
||
"""
|
||
Analyse un shapefile pour détecter les erreurs topologiques et compte les occurrences par type.
|
||
|
||
Args:
|
||
file_path (str): Chemin vers le shapefile à analyser.
|
||
|
||
Returns:
|
||
tuple:
|
||
- dict: clé = type d'erreur, valeur = liste des indices de records concernés
|
||
- int: nombre total d'erreurs détectées
|
||
"""
|
||
error_details = {}
|
||
record_types = {}
|
||
total_records = 0
|
||
total_errors = 0
|
||
|
||
try:
|
||
if not os.path.exists(file_path):
|
||
log.error(f"File not found: {Colors.ENDC}{file_path}")
|
||
return {}, -1
|
||
|
||
driver = ogr.GetDriverByName("ESRI Shapefile")
|
||
datasource = driver.Open(file_path, 0) # 0 = read-only
|
||
|
||
if datasource is None:
|
||
log.error(f"Cannot open file: {Colors.ENDC}{file_path}")
|
||
return {}, -1
|
||
|
||
layer = datasource.GetLayer()
|
||
|
||
for i, feature in enumerate(layer):
|
||
total_records += 1
|
||
|
||
if feature is None:
|
||
log.error(
|
||
f"Error in file {Colors.ENDC}{safe_relpath(file_path)}{Colors.ERROR}, "
|
||
f"record {Colors.ENDC}{i+1}{Colors.ERROR} is None"
|
||
)
|
||
globalDat.errorCount += 1
|
||
continue
|
||
|
||
geometry = feature.GetGeometryRef()
|
||
|
||
# Vérifier présence géométrie
|
||
if geometry is None:
|
||
log.error(
|
||
f"Error in file {Colors.ENDC}{safe_relpath(file_path)}{Colors.ERROR}, "
|
||
f"record {Colors.ENDC}{i+1}{Colors.ERROR} has no geometry, correct it : "
|
||
f"_ID: {Colors.ENDC}{feature.GetField('_ID')}{Colors.ERROR}, "
|
||
f"_NAME: {Colors.ENDC}{feature.GetField('_NAME')}{Colors.ERROR}, "
|
||
f"_SURVEY: {Colors.ENDC}{feature.GetField('_SURVEY')}{Colors.ENDC}"
|
||
)
|
||
globalDat.errorCount += 1
|
||
continue
|
||
|
||
# Ignorer géométries vides
|
||
if geometry.IsEmpty():
|
||
log.warning(
|
||
f"Warning, file {Colors.ENDC}{safe_relpath(file_path)}{Colors.WARNING}, "
|
||
f"Record {i+1} has empty geometry. Skipping.{Colors.ENDC}"
|
||
)
|
||
continue
|
||
|
||
# Comptage des types de géométrie
|
||
geom_type = geometry.GetGeometryName()
|
||
record_types[geom_type] = record_types.get(geom_type, 0) + 1
|
||
|
||
# Vérification topologique
|
||
try:
|
||
if not geometry.IsValid():
|
||
total_errors += 1
|
||
|
||
# Tentative d'explication (GEOS requis dans GDAL)
|
||
try:
|
||
validity_explanation = geometry.IsValidReason()
|
||
except Exception:
|
||
validity_explanation = "Invalid Geometry"
|
||
|
||
error_details.setdefault(validity_explanation, []).append(i)
|
||
|
||
except Exception as e:
|
||
log.error(
|
||
f"Error in file {Colors.ENDC}{safe_relpath(file_path)}{Colors.ERROR}, "
|
||
f"validating geometry for record {Colors.ENDC}{i+1}{Colors.ERROR}: "
|
||
f"{Colors.ENDC}{e}{Colors.ENDC}"
|
||
)
|
||
globalDat.errorCount += 1
|
||
|
||
log.info(
|
||
f"Geometry num: {Colors.ENDC}{total_records}{Colors.YELLOW}, "
|
||
f"types found: {Colors.ENDC}{record_types}"
|
||
)
|
||
|
||
if total_errors == 0:
|
||
log.info(
|
||
f"File error check OK: {Colors.ENDC}{safe_relpath(file_path)}{Colors.GREEN}, "
|
||
f"records: {Colors.ENDC}{total_records}{Colors.GREEN}, no errors found"
|
||
)
|
||
else:
|
||
log.error(
|
||
f"File error check NOK: {Colors.ENDC}{safe_relpath(file_path)}{Colors.ERROR}, "
|
||
f"records: {Colors.ENDC}{total_records}{Colors.ERROR}, "
|
||
f"total errors: {Colors.ENDC}{total_errors}"
|
||
)
|
||
globalDat.errorCount += 1
|
||
|
||
log.info(
|
||
f"Geometry in file: {Colors.ENDC}{safe_relpath(file_path)}{Colors.GREEN}, "
|
||
f"types found: {Colors.ENDC}{record_types}"
|
||
)
|
||
|
||
datasource = None # fermeture propre
|
||
|
||
return error_details, total_errors
|
||
|
||
except Exception as e:
|
||
log.error(
|
||
f"Topology error when analyzing the shapefile: {Colors.ENDC}"
|
||
f"{safe_relpath(file_path)}{Colors.ERROR}, code: {Colors.ENDC}{e}"
|
||
)
|
||
globalDat.errorCount += 1
|
||
return {}, -1
|
||
|
||
#################################################################################################
|
||
def ThtoQGis(pathshp, outputspath):
|
||
|
||
# Check if areas, lines, points2d and outline shapefiles exists...
|
||
|
||
# Check if Outputs path exists
|
||
if not os.path.exists(outputspath):
|
||
log.warning(f"WARNING: {Colors.ENDC}{safe_relpath(outputspath)}{Colors.WARNING} does not exist, I am creating it...")
|
||
os.mkdir(outputspath)
|
||
|
||
file_list = ['points2d', 'lines2d', 'outline2d', 'areas2d', 'walls3d', 'stations3d', 'shots3d']
|
||
dest_list = ['points2d', 'outline2d', 'walls3d', 'stations3d', 'shots3d']
|
||
|
||
log.info(f"{Colors.HEADER}{Colors.UNDERLINE}Step 1: test files and convert to GPKG format in the folder:{Colors.ENDC} {safe_relpath(outputspath)}")
|
||
|
||
count = 0
|
||
|
||
for fname in file_list:
|
||
count+= 1
|
||
log.info(f"Working with file ({Colors.ENDC}{count}/{len(file_list)}{Colors.INFO}): {Colors.ENDC}{fname}.shp")
|
||
|
||
file = os.path.join(pathshp, fname + '.shp')
|
||
|
||
if not os.path.isfile(file):
|
||
log.error(f"ERROR the file {Colors.ENDC}{(str(file))}{Colors.ERROR} does not exist'{Colors.ENDC}")
|
||
globalDat.errorCount += 1
|
||
continue
|
||
|
||
err, valid = diagnostic(file)
|
||
|
||
if fname in dest_list :
|
||
destinationName = fname
|
||
|
||
else :
|
||
destinationName = fname + '_fixed'
|
||
|
||
shp2gpkg(pathshp, fname, outputspath, destinationName)
|
||
|
||
if err != 0 :
|
||
err2, valid2 = diagnostic(os.path.join(outputspath,destinationName + '.gpkg'))
|
||
|
||
if err2 != 0 :
|
||
log.error(f"in file {Colors.ENDC}{(safe_relpath(outputspath + destinationName + '.gpkg'))}{Colors.ERROR} please fix it manually with QGis...")
|
||
globalDat.errorCount += 1
|
||
continue
|
||
|
||
|
||
log.info(f"{Colors.HEADER}{Colors.UNDERLINE}Step 2: adapte drawing files (cut it) for QGis in the folder:{Colors.ENDC} {safe_relpath(outputspath)}")
|
||
|
||
## Work with lines
|
||
file_path = os.path.join(outputspath, 'lines2d_fixed.gpkg')
|
||
valid = cutGPKG(file_path, os.path.join(outputspath,'outline2d.gpkg'), os.path.join(outputspath,'lines2dMasked.gpkg'))
|
||
err, valid2 = diagnostic(os.path.join(outputspath,'lines2dMasked.gpkg'))
|
||
|
||
if (valid2 - valid) != 0 :
|
||
log.warning(f"{Colors.ENDC}{abs(valid2 - valid)}{Colors.WARNING} deleted geometries need to be verified in {Colors.ENDC}lines2dMasked.gpkg{Colors.WARNING} file")
|
||
elif (valid2 == -1):
|
||
log.error(f"in clipped geometries in {Colors.ENDC}lines2dMasked.gpkg{Colors.INFO} file")
|
||
else :
|
||
log.info(f"{Colors.ENDC}{valid}{Colors.INFO} clipped geometries in {Colors.ENDC}lines2dMasked.gpkg{Colors.ERROR} file")
|
||
|
||
if os.path.exists(file_path):
|
||
os.remove(file_path)
|
||
|
||
## Work with Areas
|
||
file_path = os.path.join(outputspath, 'areas2d_fixed.gpkg')
|
||
valid = cutGPKG(file_path, os.path.join(outputspath,'outline2d.gpkg'), os.path.join(outputspath,'areas2dMasked.gpkg'))
|
||
err, valid2 = diagnostic(os.path.join(outputspath,'areas2dMasked.gpkg'))
|
||
|
||
if (valid2 - valid) != 0 :
|
||
log.warning(f"{Colors.ENDC}{abs(valid2 - valid)}{Colors.WARNING} Deleted geometries need to be verified in {Colors.ENDC}areas2d_fixed.gpkg{Colors.WARNING} file")
|
||
elif (valid2 == -1):
|
||
log.error(f"in clipped geometries in {Colors.ENDC}areas2d_fixed.gpkg{Colors.ERROR} file")
|
||
else :
|
||
log.info(f"{Colors.ENDC}{valid}{Colors.INFO} clipped geometries in {Colors.ENDC}areas2d_fixed.gpkg{Colors.INFO} file")
|
||
|
||
if os.path.exists(file_path):
|
||
os.remove(file_path)
|
||
|
||
## Work with Points 'add altitudes'
|
||
# extractVertices(os.path.join(outputspath,'lines2dMasked.gpkg'), os.path.join(outputspath,'points2d.gpkg'))
|
||
# diagnostic(os.path.join(outputspath,'points2d.gpkg'))
|
||
|
||
|
||
|
||
#####################################################################################################################################
|
||
# #
|
||
# Main #
|
||
# #
|
||
#####################################################################################################################################
|
||
if __name__ == u'__main__':
|
||
#################################################################################################
|
||
ogr.UseExceptions()
|
||
gdal.UseExceptions()
|
||
gdal.PushErrorHandler("CPLQuietErrorHandler")
|
||
gdal.SetConfigOption("SHAPE_ENCODING", "UTF-8")
|
||
gdal.SetConfigOption("OGR_CHARSET", "UTF-8")
|
||
gdal.SetConfigOption("OGR_GPKG_ENCODING", "UTF-8")
|
||
|
||
globalDat.errorCount = 0
|
||
input_folder_name =""
|
||
start_time = time.time()
|
||
|
||
#################################################################################################
|
||
# Parse arguments #
|
||
#################################################################################################
|
||
parser = argparse.ArgumentParser(
|
||
description=f"{Colors.HEADER}Script to generate QGis (.gpkg) files from Therion (.shp) files with auto-correction if possible",
|
||
formatter_class=argparse.RawTextHelpFormatter)
|
||
|
||
parser.print_help = colored_help.__get__(parser)
|
||
parser.add_argument(
|
||
'--option',
|
||
default="auto",
|
||
choices=["auto", "manual", "test"],
|
||
help=(
|
||
f"Execution options for pyThtoQgis.py\n"
|
||
f"auto\t-> Execution from the folder {globalDat.pathshp} (défaut)\n"
|
||
f"manual\t-> Manual selection by a window box for the input folder\n"
|
||
f"test\t-> Tests fonction (debug)\n"
|
||
)
|
||
)
|
||
|
||
parser.add_argument(
|
||
'--folder',
|
||
type=str,
|
||
help="Input folder containing the shapefiles to process"
|
||
)
|
||
|
||
|
||
parser.epilog = (
|
||
f"{Colors.HEADER}Note : {Colors.GREEN}to generate shp files in therion, add in thconfig file the commande\n"
|
||
f"\t{Colors.GREEN}-> {Colors.ENDC}export model -fmt esri -o Outputs/SHP/ -enc UTF-8{Colors.ENDC}\n"
|
||
f"\n"
|
||
f"{Colors.HEADER}Usage examples :{Colors.ENDC}\n"
|
||
f"\t{Colors.GREEN}-> {Colors.ENDC}python pyThtoQgis.py --folder \"../../Outputs/SHP/\"{Colors.ENDC}\n"
|
||
f"\t{Colors.GREEN}-> {Colors.ENDC}python pyThtoQgis.py --option manual{Colors.ENDC}\n"
|
||
)
|
||
|
||
# Analyser les arguments de ligne de commande
|
||
args = parser.parse_args()
|
||
|
||
if os.name == 'posix': os.system('clear') # Linux, MacOS
|
||
elif os.name == 'nt': os.system('cls')# Windows
|
||
else: print("\n" * 100)
|
||
|
||
#################################################################################################
|
||
if args.folder :
|
||
|
||
input_folder = os.path.normpath(args.folder)
|
||
output_folder = os.path.join(input_folder,globalDat.outputfolder)
|
||
|
||
if not os.path.exists(input_folder):
|
||
log = setup_logger(globalDat.output_log, globalDat.debug_log)
|
||
log.error(f"ERROR the folder {Colors.ENDC}{input_folder}{Colors.ERROR} does not exist'{Colors.ENDC}")
|
||
globalDat.errorCount += 1
|
||
sys.exit()
|
||
|
||
if not os.path.exists(output_folder):
|
||
os.makedirs(output_folder)
|
||
|
||
log = setup_logger(os.path.join(output_folder,globalDat.file_log), globalDat.debug_log)
|
||
|
||
log.info(f'{Colors.HEADER}*********************************************************************************************************')
|
||
log.info(f'{Colors.HEADER}Script to generate QGis (.gpkg) files from Therion (.shp) files with auto-correction if possible')
|
||
log.info(f'{Colors.HEADER} original written by X. Robert, ISTerre : {Colors.ENDC}October 2022')
|
||
log.info(f'{Colors.HEADER} updated by : {Colors.ENDC}alexandre.pont@yahoo.fr')
|
||
log.info(f'{Colors.HEADER} version : {Colors.ENDC}{globalDat.Version}')
|
||
log.info(f'{Colors.HEADER} commande line mode')
|
||
log.info(f'{Colors.HEADER} input folder : {Colors.ENDC}{safe_relpath(input_folder)}')
|
||
log.info(f'{Colors.HEADER} output folder : {Colors.ENDC}{safe_relpath(output_folder)}')
|
||
log.info(f'{Colors.HEADER} log file : {Colors.ENDC}{safe_relpath(next((h.baseFilename for h in log.handlers if isinstance(h, logging.FileHandler)), None))}')
|
||
log.info(f'{Colors.HEADER}*********************************************************************************************************')
|
||
|
||
ThtoQGis(input_folder, output_folder)
|
||
|
||
|
||
elif args.option == "auto" :
|
||
log = setup_logger(globalDat.output_log, globalDat.debug_log)
|
||
|
||
if not os.path.exists(globalDat.outputspath):
|
||
os.makedirs(globalDat.outputspath)
|
||
|
||
log.info(f'{Colors.HEADER}*********************************************************************************************************')
|
||
log.info(f'{Colors.HEADER}Script to generate QGis (.gpkg) files from Therion (.shp) files with auto-correction if possible')
|
||
log.info(f'{Colors.HEADER} original written by X. Robert, ISTerre : {Colors.ENDC}October 2022')
|
||
log.info(f'{Colors.HEADER} updated by : {Colors.ENDC}alexandre.pont@yahoo.fr')
|
||
log.info(f'{Colors.HEADER} version : {Colors.ENDC}{globalDat.Version}')
|
||
log.info(f'{Colors.HEADER} auto mode')
|
||
log.info(f'{Colors.HEADER} input folder : {Colors.ENDC}{globalDat.pathshp}')
|
||
log.info(f'{Colors.HEADER} output folder : {Colors.ENDC}{globalDat.outputspath}')
|
||
log.info(f'{Colors.HEADER}*********************************************************************************************************')
|
||
|
||
ThtoQGis(globalDat.pathshp, globalDat.outputspath)
|
||
|
||
|
||
elif args.option == "manual" :
|
||
root = tk.Tk()
|
||
root.withdraw() # Cacher la fenêtre principale de Tkinter
|
||
input_folder_name = filedialog.askdirectory( title="Choose the shp folder")
|
||
|
||
input_folder = input_folder_name + "\\"
|
||
output_folder = input_folder + globalDat.outputfolder
|
||
|
||
if not os.path.exists(output_folder):
|
||
os.makedirs(output_folder)
|
||
|
||
log = setup_logger(output_folder + globalDat.file_log, globalDat.debug_log)
|
||
|
||
if not input_folder_name:
|
||
log.error(f"No folder selected. The program will terminate")
|
||
globalDat.errorCount += 1
|
||
sys.exit()
|
||
|
||
|
||
log.info(f'{Colors.HEADER}*********************************************************************************************************')
|
||
log.info(f'{Colors.HEADER}Script to generate QGis (.gpkg) files from Therion (.shp) files with auto-correction if possible')
|
||
log.info(f'{Colors.HEADER} original written by X. Robert, ISTerre : {Colors.ENDC}October 2022')
|
||
log.info(f'{Colors.HEADER} updated by : {Colors.ENDC}alexandre.pont@yahoo.fr')
|
||
log.info(f'{Colors.HEADER} version : {Colors.ENDC}{globalDat.Version}')
|
||
log.info(f'{Colors.HEADER} manual mode')
|
||
log.info(f'{Colors.HEADER} input folder : {Colors.ENDC}{safe_relpath(input_folder)}')
|
||
log.info(f'{Colors.HEADER} output folder : {Colors.ENDC}{safe_relpath(output_folder)}')
|
||
log.info(f'{Colors.HEADER}*********************************************************************************************************')
|
||
|
||
ThtoQGis(input_folder, output_folder)
|
||
|
||
|
||
elif args.option == "test" :
|
||
|
||
log = setup_logger(globalDat.output_log, globalDat.debug_log)
|
||
|
||
if not os.path.exists(globalDat.outputspath):
|
||
os.makedirs(globalDat.outputspath)
|
||
|
||
log.info(f'{Colors.HEADER}*********************************************************************************************************')
|
||
log.info(f'{Colors.HEADER}Script to generate QGis (.gpkg) files from Therion (.shp) files with auto-correction if possible')
|
||
log.info(f'{Colors.HEADER} original written by X. Robert, ISTerre : {Colors.ENDC}October 2022')
|
||
log.info(f'{Colors.HEADER} updated by : {Colors.ENDC}alexandre.pont@yahoo.fr')
|
||
log.info(f'{Colors.HEADER} version : {Colors.ENDC}{globalDat.Version}')
|
||
log.info(f'{Colors.HEADER} test mode')
|
||
log.info(f'{Colors.HEADER} input folder : {Colors.ENDC}{globalDat.pathshp}')
|
||
log.info(f'{Colors.HEADER} output folder : {Colors.ENDC}{globalDat.outputspath}')
|
||
log.info(f'{Colors.HEADER}*********************************************************************************************************')
|
||
|
||
extractVertices(globalDat.outputspath + 'lines2dMasked.gpkg', globalDat.outputspath + 'points2d.gpkg')
|
||
|
||
exit(0)
|
||
|
||
diagnostic(globalDat.pathshp + 'lines2d.shp')
|
||
count_topology_errors(globalDat.pathshp + 'lines2d.shp')
|
||
shp2gpkg(globalDat.pathshp, 'lines2d' , globalDat.outputspath, 'lines2d')
|
||
diagnostic(globalDat.outputspath + 'lines2d.gpkg')
|
||
|
||
|
||
diagnostic(globalDat.pathshp + 'outline2d.shp')
|
||
shp2gpkg(globalDat.pathshp, 'outline2d', globalDat.outputspath, 'outline2d')
|
||
diagnostic(globalDat.outputspath + 'outline2d.gpkg')
|
||
|
||
# diagnostic(globalDat.pathshp + 'points2d.shp')
|
||
# shp2gpkg(globalDat.pathshp, 'points2d', globalDat.outputspath, 'points2d')
|
||
# diagnostic(globalDat.outputspath + 'points2d.gpkg')
|
||
|
||
diagnostic(globalDat.pathshp + 'areas2d.shp')
|
||
shp2gpkg(globalDat.pathshp, 'areas2d', globalDat.outputspath, 'areas2d')
|
||
diagnostic(globalDat.outputspath + 'areas2d.gpkg')
|
||
|
||
# diagnostic(globalDat.pathshp + 'walls3d.shp')
|
||
# shp2gpkg(globalDat.pathshp, 'walls3d', globalDat.outputspath, 'walls3d')
|
||
# diagnostic(globalDat.outputspath + 'walls3d.gpkg')
|
||
|
||
cutGPKG(globalDat.outputspath + 'lines2d.gpkg', globalDat.outputspath + 'outline2d.gpkg', globalDat.outputspath + 'lines2dMasked.gpkg')
|
||
diagnostic(globalDat.outputspath + 'lines2dMasked.gpkg')
|
||
|
||
cutGPKG(globalDat.outputspath + 'areas2d.gpkg', globalDat.outputspath + 'outline2d.gpkg', globalDat.outputspath + 'areas2dMasked.gpkg')
|
||
diagnostic(globalDat.outputspath + 'areas2dMasked.gpkg')
|
||
|
||
|
||
# outlines = gpd.read_file(globalDat.outputspath + 'outline2d.gpkg')
|
||
# cutLines(globalDat.outputspath, globalDat.outputspath + 'outline2d.gpkg', globalDat.outputspath)
|
||
# diagnostic(globalDat.outputspath + 'lines2dMasked.gpkg')
|
||
|
||
# fname = "stations3d"
|
||
# shp2gpkg(globalDat.pathshp, fname , globalDat.outputspath, fname)
|
||
|
||
# fname = "shots3d"
|
||
# shp2gpkg(globalDat.pathshp, fname , globalDat.outputspath, fname)
|
||
|
||
# fname = "walls3d"
|
||
# shp2gpkg(globalDat.pathshp, fname , globalDat.outputspath, fname)
|
||
|
||
#################################################################################################
|
||
elapsed = time.time() - start_time
|
||
|
||
if globalDat.errorCount == 0 :
|
||
log.info(f"{Colors.HEADER}=========================================================================================================")
|
||
log.info(f"{Colors.HEADER}Execution completed without errors in {Colors.ENDC}{elapsed:.2f}{Colors.HEADER} s")
|
||
log.info(f"{Colors.HEADER}=========================================================================================================")
|
||
|
||
else :
|
||
log.error(f"{Colors.HEADER}=========================================================================================================")
|
||
log.error(f"{Colors.HEADER}Execution completed with {Colors.ENDC}{globalDat.errorCount}{Colors.ERROR} errors in {Colors.ENDC}{elapsed:.2f}{Colors.HEADER} s")
|
||
for i, error in enumerate(globalDat.geometryErrors, start=1):
|
||
log.error(f"{error}")
|
||
log.error(f"{Colors.HEADER}=========================================================================================================")
|
||
|
||
|
||
|
||
|
||
|