pyThtoQgis

This commit is contained in:
Alex38Lyon
2026-01-12 19:58:16 +01:00
parent 82979e2a63
commit db516b2cc0
98 changed files with 139527 additions and 137783 deletions
+8
View File
@@ -1,6 +1,14 @@
Scripts for Therion
====================
pyThtoQGis
---------
Script to convert (.shp) Therion file in (.qkpg) files for Qgis
Usage : python pyThtoGgis.py
pyThtoDat
---------
+9
View File
@@ -3,6 +3,15 @@ Scripts pour Therion
🇬🇧 [Read in English](./README.en.md)
pyThtoQGis
---------
Script pour convertir les fichiers (.shp) produit par Therion en fichiers (.qkpg) pour Qgis
Usage : python pyThtoGgis.py
pyThtoDat
---------
-33
View File
@@ -1,33 +0,0 @@
2026-01-08 10:01:05,741 - INFO - ################################################################################################################################################################
2026-01-08 10:01:05,741 - INFO - # Conversion Th, Dat, Mak, Tro, files to Therion files and folders #
2026-01-08 10:01:05,741 - INFO - # Script pyCreateTh by : alexandre.pont@yahoo.fr #
2026-01-08 10:01:05,742 - INFO - # Version : 2026.01.08 #
2026-01-08 10:01:05,744 - INFO - # Input file : . #
2026-01-08 10:01:05,744 - INFO - # Output folder : . #
2026-01-08 10:01:05,744 - INFO - # Log file : pyCreateTh.log #
2026-01-08 10:01:05,744 - INFO - # Config file: config.ini #
2026-01-08 10:01:05,744 - INFO - # #
2026-01-08 10:01:05,745 - INFO - ################################################################################################################################################################
2026-01-08 10:01:05,745 - CRITICAL - !!! No valid file selected
2026-01-08 10:02:43,193 - INFO - ######################################################################################################################################################
2026-01-08 10:02:43,193 - INFO - # Conversion Th, Dat, Mak, Tro, files to Therion files and folders #
2026-01-08 10:02:43,194 - INFO - # Script pyCreateTh by : alexandre.pont@yahoo.fr #
2026-01-08 10:02:43,194 - INFO - # Version : 2026.01.08 #
2026-01-08 10:02:43,194 - INFO - # Input file : . #
2026-01-08 10:02:43,194 - INFO - # Output folder : . #
2026-01-08 10:02:43,194 - INFO - # Log file : pyCreateTh.log #
2026-01-08 10:02:43,195 - INFO - # Config file: config.ini #
2026-01-08 10:02:43,195 - INFO - # #
2026-01-08 10:02:43,195 - INFO - ######################################################################################################################################################
2026-01-08 10:02:43,195 - CRITICAL - !!! No valid file selected, try again
2026-01-08 10:03:35,413 - INFO - ######################################################################################################################################################
2026-01-08 10:03:35,413 - INFO - # Conversion th, dat, mak, tro and trox, files to therion files #
2026-01-08 10:03:35,414 - INFO - # Script pyCreateTh by : alexandre.pont@yahoo.fr #
2026-01-08 10:03:35,414 - INFO - # Version : 2026.01.08 #
2026-01-08 10:03:35,414 - INFO - # Input file : . #
2026-01-08 10:03:35,414 - INFO - # Output folder : . #
2026-01-08 10:03:35,415 - INFO - # Log file : pyCreateTh.log #
2026-01-08 10:03:35,415 - INFO - # Config file: config.ini #
2026-01-08 10:03:35,415 - INFO - # #
2026-01-08 10:03:35,416 - INFO - ######################################################################################################################################################
2026-01-08 10:03:35,416 - CRITICAL - !!! No valid file selected, try again
+24
View File
@@ -0,0 +1,24 @@
{
"cSpell.words": [
"cutareas",
"ENDC",
"esri",
"fiona",
"ftype",
"geodf",
"geopandas",
"Ggis",
"gpkg",
"infile",
"Masekd",
"outputspath",
"pathshp",
"polygonize",
"pygeos",
"Qgis",
"shapefiles",
"thconfig",
"therion",
"writerecords"
]
}
Binary file not shown.
+13
View File
@@ -0,0 +1,13 @@
PROJCS["ED_1950_UTM_Zone_30N",
GEOGCS["GCS_European_1950",
DATUM["D_European_1950",
SPHEROID["International_1924",6378388.0,297.0]],
PRIMEM["Greenwich",0.0],
UNIT["Degree",0.0174532925199433]],
PROJECTION["Transverse_Mercator"],
PARAMETER["False_Easting",500000.0],
PARAMETER["False_Northing",0.0],
PARAMETER["Central_Meridian",-3.0],
PARAMETER["Scale_Factor",0.9996],
PARAMETER["Latitude_Of_Origin",0.0],
UNIT["Meter",1.0]]
Binary file not shown.
Binary file not shown.
Binary file not shown.
+13
View File
@@ -0,0 +1,13 @@
PROJCS["ED_1950_UTM_Zone_30N",
GEOGCS["GCS_European_1950",
DATUM["D_European_1950",
SPHEROID["International_1924",6378388.0,297.0]],
PRIMEM["Greenwich",0.0],
UNIT["Degree",0.0174532925199433]],
PROJECTION["Transverse_Mercator"],
PARAMETER["False_Easting",500000.0],
PARAMETER["False_Northing",0.0],
PARAMETER["Central_Meridian",-3.0],
PARAMETER["Scale_Factor",0.9996],
PARAMETER["Latitude_Of_Origin",0.0],
UNIT["Meter",1.0]]
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
ISO-8859-1
Binary file not shown.
@@ -0,0 +1 @@
PROJCS["ED_1950_UTM_Zone_30N",GEOGCS["GCS_European_1950",DATUM["D_European_1950",SPHEROID["International_1924",6378388.0,297.0]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",500000.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",-3.0],PARAMETER["Scale_Factor",0.9996],PARAMETER["Latitude_Of_Origin",0.0],UNIT["Meter",1.0]]
Binary file not shown.
Binary file not shown.
Binary file not shown.
+13
View File
@@ -0,0 +1,13 @@
PROJCS["ED_1950_UTM_Zone_30N",
GEOGCS["GCS_European_1950",
DATUM["D_European_1950",
SPHEROID["International_1924",6378388.0,297.0]],
PRIMEM["Greenwich",0.0],
UNIT["Degree",0.0174532925199433]],
PROJECTION["Transverse_Mercator"],
PARAMETER["False_Easting",500000.0],
PARAMETER["False_Northing",0.0],
PARAMETER["Central_Meridian",-3.0],
PARAMETER["Scale_Factor",0.9996],
PARAMETER["Latitude_Of_Origin",0.0],
UNIT["Meter",1.0]]
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
ISO-8859-1
Binary file not shown.
@@ -0,0 +1 @@
PROJCS["ED_1950_UTM_Zone_30N",GEOGCS["GCS_European_1950",DATUM["D_European_1950",SPHEROID["International_1924",6378388.0,297.0]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",500000.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",-3.0],PARAMETER["Scale_Factor",0.9996],PARAMETER["Latitude_Of_Origin",0.0],UNIT["Meter",1.0]]
Binary file not shown.
Binary file not shown.
Binary file not shown.
+13
View File
@@ -0,0 +1,13 @@
PROJCS["ED_1950_UTM_Zone_30N",
GEOGCS["GCS_European_1950",
DATUM["D_European_1950",
SPHEROID["International_1924",6378388.0,297.0]],
PRIMEM["Greenwich",0.0],
UNIT["Degree",0.0174532925199433]],
PROJECTION["Transverse_Mercator"],
PARAMETER["False_Easting",500000.0],
PARAMETER["False_Northing",0.0],
PARAMETER["Central_Meridian",-3.0],
PARAMETER["Scale_Factor",0.9996],
PARAMETER["Latitude_Of_Origin",0.0],
UNIT["Meter",1.0]]
Binary file not shown.
Binary file not shown.
Binary file not shown.
+13
View File
@@ -0,0 +1,13 @@
PROJCS["ED_1950_UTM_Zone_30N",
GEOGCS["GCS_European_1950",
DATUM["D_European_1950",
SPHEROID["International_1924",6378388.0,297.0]],
PRIMEM["Greenwich",0.0],
UNIT["Degree",0.0174532925199433]],
PROJECTION["Transverse_Mercator"],
PARAMETER["False_Easting",500000.0],
PARAMETER["False_Northing",0.0],
PARAMETER["Central_Meridian",-3.0],
PARAMETER["Scale_Factor",0.9996],
PARAMETER["Latitude_Of_Origin",0.0],
UNIT["Meter",1.0]]
Binary file not shown.
Binary file not shown.
Binary file not shown.
+13
View File
@@ -0,0 +1,13 @@
PROJCS["ED_1950_UTM_Zone_30N",
GEOGCS["GCS_European_1950",
DATUM["D_European_1950",
SPHEROID["International_1924",6378388.0,297.0]],
PRIMEM["Greenwich",0.0],
UNIT["Degree",0.0174532925199433]],
PROJECTION["Transverse_Mercator"],
PARAMETER["False_Easting",500000.0],
PARAMETER["False_Northing",0.0],
PARAMETER["Central_Meridian",-3.0],
PARAMETER["Scale_Factor",0.9996],
PARAMETER["Latitude_Of_Origin",0.0],
UNIT["Meter",1.0]]
Binary file not shown.
Binary file not shown.
Binary file not shown.
+13
View File
@@ -0,0 +1,13 @@
PROJCS["ED_1950_UTM_Zone_30N",
GEOGCS["GCS_European_1950",
DATUM["D_European_1950",
SPHEROID["International_1924",6378388.0,297.0]],
PRIMEM["Greenwich",0.0],
UNIT["Degree",0.0174532925199433]],
PROJECTION["Transverse_Mercator"],
PARAMETER["False_Easting",500000.0],
PARAMETER["False_Northing",0.0],
PARAMETER["Central_Meridian",-3.0],
PARAMETER["Scale_Factor",0.9996],
PARAMETER["Latitude_Of_Origin",0.0],
UNIT["Meter",1.0]]
Binary file not shown.
Binary file not shown.
+453
View File
@@ -0,0 +1,453 @@
######!/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 #
# #
# By Xavier Robert #
# Grenoble, october 2022 #
# #
#############################################################
Written by Xavier Robert, octobert 2022
xavier.robert@ird.fr
"""
# Do divisions with Reals, not with integers
# Must be at the beginning of the file
from __future__ import division
# Import Python modules
#import numpy as np
import fiona
import shapely
from shapely.geometry import Polygon, LineString
import geopandas as gpd
import pandas as pd
import sys, os, copy, shutil
#from functools import wraps
from alive_progress import alive_bar # https://github.com/rsalmei/alive-progress
###### TO DO #####
# -
##### End TO DO #####
#################################################################################################
#################################################################################################
#def validate(func):
# """
# Function to validate areas topology.
# From https://shapely.readthedocs.io/en/latest/manual.html
# Args:
# func (_type_): _description_
# Raises:
# TopologicalError: Error of topology
# - area does not close
# - inner ring
# - boundaries intersects
# Returns:
# _type_: _description_
# """
# @wraps(func)
# def wrapper(*args, **kwargs):
# ob = func(*args, **kwargs)
# if not ob.is_valid:
# raise TopologicalError(
# "Given arguments do not determine a valid geometric object")
# return ob
# return wrapper
def validate(inputfile, rec):
rec2 = rec
#print(rec['geometry']['coordinates'][0]) # il y a visiblement un soucis avec le nombre de []
if not Polygon(rec['geometry']['coordinates'][0]).is_valid:
print('Problem in %s geometry' %(inputfile))
print('%s is not a valid geometric object' %(rec['properties']['_ID']))
raise TopologicalError('\033[91mERROR:\033[00m Correction does not work...\n%s is not a valid geometric object\n\t The error is: %s' %(str(rec['properties']['_ID']), shapely.validation.explain_validity(rec)))
#print('We try to correct it')
#rec2b = shapely.validation.make_valid(Polygon(rec['geometry']['coordinates'][0]))
# Check à améliorer, il faut que ce soit un Polygon, et non un MultiPolygon...
#if not rec2b.is_valid:
# raise TopologicalError('ERROR: Correction failed...\n%s is not a valid geometric object\n\t The error is: %s' %(str(rec['properties']['_ID']), shapely.validation.explain_validity(rec)))
#else:
# rec2['geometry']['coordinates'][0] = list(rec2b.exterior.coords)
# Find where there is the error if possible
#Diagnostics
#validation.explain_validity(ob):
#Returns a string explaining the validity or invalidity of the object.
#The messages may or may not have a representation of a problem point that can be parsed out.
#coords = [(0, 0), (0, 2), (1, 1), (2, 2), (2, 0), (1, 1), (0, 0)]
#p = Polygon(coords)
#from shapely.validation import explain_validity
#shapely.validation.explain_validity(p)
#'Ring Self-intersection[1 1]'
#shapely.validation.make_valid(ob)
#Returns a valid representation of the geometry, if it is invalid. If it is valid, the input geometry will be returned.
#In many cases, in order to create a valid geometry, the input geometry must be split into multiple parts or multiple geometries. If the geometry must be split into multiple parts of the same geometry type, then a multi-part geometry (e.g. a MultiPolygon) will be returned. if the geometry must be split into multiple parts of different types, then a GeometryCollection will be returned.
#For example, this operation on a geometry with a bow-tie structure:
#from shapely.validation import make_valid
#coords = [(0, 0), (0, 2), (1, 1), (2, 2), (2, 0), (1, 1), (0, 0)]
#p = Polygon(coords)
#make_valid(p)
#<MULTIPOLYGON (((1 1, 0 0, 0 2, 1 1)), ((2 0, 1 1, 2 2, 2 0)))>
#Yields a MultiPolygon with two parts, and sometimes area + line:
return rec2
#################################################################################################
def cutareas(pathshp, outlines, outputspath):
"""
Function to cut shapefiles areas with the outline to only keep the lines inside the outline
Args:
pathshp (str) : path where are stored output shp from Therion
outlines (geopandas obj): the outline shapefile
outputspath (str) : path where to copy the gpkg files
"""
print('Working with areas...')
# 2- Validate the outline and Areas shapefile
#for rec in outlines:
# rec2 = validate('outline2d.shp', rec)
# # update correction --> To do ?
# #if rec2 != rec:
#for rec in areas:
# rec2 = validate('areas2d.shp', rec)
# # update correction
# #if rec2 != rec:
# Read the Line Shapefile
areas = gpd.read_file(pathshp + 'areas2d.shp', driver = 'ESRI shapefile')
# Extract the intersections between outlines and lines
# be careful, for this operation, geopandas needs to work with rtree and not pygeos
# --> uninstall pygeos and install rtree
try:
areasIN = areas.overlay(outlines, how = 'intersection')
except:
print('ERROR: 1) uninstall pygeos and install rtree\n\t2) check your polygons validity')
import rtree
print ('\tYou may check the validity of your polygons with the verify function in QGIS')
areasIN = areas.overlay(outlines, how = 'intersection')
# Removes inner lines that have different id and scrap_id
areasIN = areasIN[areasIN['_SCRAP_ID'] == areasIN ['_ID']]
# Save output
#areasIN.to_file("areas2dMasekd.gpkg", driver = "GPKG", encoding = 'utf8')
areasIN.to_file(outputspath + "areas2dMasekd.gpkg", driver = "GPKG")
return
#################################################################################################
def cutLines(pathshp, outlines, outputspath):
"""
Function to cut shapefiles lines with the outline to only keep the lines inside the outline
Args:
pathshp (str) : path where are stored output shp from Therion
outlines (geopandas obj): the outline shapefile
outputspath (str) : path where to copy the gpkg files
"""
print('Working with lines...')
# Read the Line Shapefile
lines = gpd.read_file(pathshp + 'lines2d.shp', driver = 'ESRI shapefile')
# Extract lines that are not masked by the outline
linesOUT = pd.concat((lines[lines['_TYPE'] == 'centerline'],
lines[lines['_TYPE'] == 'water_flow'],
lines[lines['_TYPE'] == 'label'],
lines[lines['_CLIP'] == 'off']),
ignore_index=True)
# Extract lines will be masked by the outline
linesIN = lines[lines['_CLIP'] != 'off']
linesIN = linesIN[linesIN['_TYPE'] != 'centerline']
linesIN = linesIN[linesIN['_TYPE'] != 'water_flow']
linesIN = linesIN[linesIN['_TYPE'] != 'label']
# Extract the intersections between outlines and lines
# be careful, for this operation, geopandas needs to work with rtree and not pygeos
# --> uninstall pygeos and install rtree
try:
linesIN = linesIN.overlay(outlines, how = 'intersection', keep_geom_type=True)
except:
print('\033[91mERROR: 1\033[00m) uninstall pygeos and install rtree\n\t2) check your polygons validity')
import rtree
print ('\tYou may check the validity of your polygons with the verify function in QGIS')
linesIN = linesIN.overlay(outlines, how = 'intersection', keep_geom_type=True)
print('TEST')
# Removes inner lines that have different id and scrap_id
linesIN = linesIN[linesIN['_SCRAP_ID'] == linesIN ['_ID']]
# Merge the IN and OUT database
linesTOT = pd.concat((linesOUT, linesIN),
ignore_index=True)
# Save output
#linesTOT.to_file("lines2dMasekd.gpkg", driver="GPKG", encoding = 'utf8')
linesTOT.to_file(outputspath + "lines2dMasekd.gpkg", driver="GPKG")
return
#################################################################################################
def AddAltPoint(pathshp, outputspath):
"""
Function to add the altitude of the stations and entrances in the attribut table
Args:
pathshp (str) : path where are stored output shp from Therion
outputspath (str): path where to copy the gpkg files
"""
print('Working with points...')
# Definition des altitudes des entrées supérieures des réseaux à plusieurs entrées
EntreeSupp = {'JB' : 2333, # Entrée C37
'CP' : 2136, # Entrée CP16
'LP9' : 2299, # Entrée LP9
'CP6' : 2182, # Entrée CP53
'CP62' : 1960, # Entrée CP62
'A21' : 1797, # Entrée A21
'Mirolda': 2330 # Entrée Jockers
}
# Définition des noms de réseau
RNames = {'JB' : 'Gouffre Jean Bernard',
'CP' : 'Réseau de la Combe aux Puaires',
'LP9' : 'LP9 - CP39',
'CP6' : 'CP6 - CP53',
'CP62' : 'CP62 - CP63',
'A21' : 'A21 -A24',
'Mirolda': 'Réseau Lucien-Bouclier - Mirolda'
}
# Définition des noms de systèmes
SNames = {'SynclinalJB' : 'Système du Jean-Bernard',
'SystemeCP' : 'Système de la Combe aux Puaires',
'SystemeAV' : 'Système des Avoudrues',
'SystemeA21' : 'Système du A21',
'SystemMirolda' : 'Système du Criou - Mirolda',
'SystemeBossetan': 'Système de Bossetan',
'sources' : 'Résurgences',
'tuet' : 'Système du Tuet',
'eauxfroides' : 'Système des Eaux Froides'
}
# Open the text file with the coordinates of the caves
# This text file (Caves.txt) should be build with Therion compilation
# and stored in the output's shapefiles folder
# export cave-list -location on -o Outputs/SHP/Caves.txt
f = open(pathshp + 'Caves.txt', 'r').readlines()
# Make a new shapefile instance
with fiona.open(pathshp + 'points2d.shp', 'r') as inputshp:
# Créer le nouveau schéma des shapefiles
newschema = inputshp.schema
newschema['properties']['_CAVE'] = 'str'
newschema['properties']['_SYSTEM'] = 'str'
newschema['properties']['_ALT'] = 'str:4'
newschema['properties']['_DEPTH'] = 'float'
newschema['properties']['_EASTING'] = 'float'
newschema['properties']['_NORTHING'] = 'float'
# Open the output shapefile
#with fiona.open(inputfile[:-4] + 'Alt.shp', 'w', crs=inputshp.crs, driver='ESRI Shapefile', schema=newschema) as ouput:
#with fiona.open('points2dAlt.gpkg', 'w', crs=inputshp.crs, driver='GPKG', schema=newschema, encoding = 'utf8') as ouput:
with fiona.open(outputspath + 'points2dAlt.gpkg', 'w', crs=inputshp.crs, driver='GPKG', schema=newschema) as ouput:
with alive_bar(len(inputshp), title = "\x1b[32;1m- Processing stations...\x1b[0m", length = 20) as bar:
# do a loop on the stations
for rec in inputshp:
# Copy the schema from the input data
g = rec
g['properties']['_CAVE'] = ''
g['properties']['_SYSTEM'] = ''
g['properties']['_DEPTH'] = ''
# Add Alt, Easting, Northing
g['properties']['_ALT'] = str(round(float(rec['geometry']['coordinates'][2])))
g['properties']['_EASTING'] = float(rec['geometry']['coordinates'][0])
g['properties']['_NORTHING'] = float(rec['geometry']['coordinates'][1])
if rec['properties']['_TYPE'] == 'station' and rec['properties']['_STSURVEY'] != None:
# Find system
system = rec['properties']['_STSURVEY'].split('.')[-2]
g['properties']['_SYSTEM'] = SNames[system]
# Find Cave
xxx = rec['properties']['_STSURVEY'].split('.')
while len(xxx) < 4:
xxx.append('junk')
if 'trous' in xxx[0] or SNames[system] == 'Résurgences' or 'sources' in xxx[0]:
g['properties']['_CAVE'] = rec['properties']['_STNAME']
g['properties']['_DEPTH'] = 0
elif 'eauxfroides' in xxx[-3]:
g['properties']['_CAVE'] = 'Résurgence des Eaux Froides'
g['properties']['_DEPTH'] = 0
elif 'tuet' in xxx[-4]:
g['properties']['_CAVE'] = 'Tuet'
g['properties']['_DEPTH'] = 0
elif 'ReseauCP' in xxx[-4]:
g['properties']['_CAVE'] = RNames['CP']
g['properties']['_DEPTH'] = EntreeSupp['CP'] - float(rec['geometry']['coordinates'][2])
elif 'LP9' in xxx[-4]:
g['properties']['_CAVE'] = RNames['LP9']
g['properties']['_DEPTH'] = EntreeSupp['LP9'] - float(rec['geometry']['coordinates'][2])
elif 'CP6' in xxx[-4]:
g['properties']['_CAVE'] = RNames['CP6']
g['properties']['_DEPTH'] = EntreeSupp['CP6'] - float(rec['geometry']['coordinates'][2])
elif 'CP62' in xxx[-4]:
g['properties']['_CAVE'] = RNames['CP62']
g['properties']['_DEPTH'] = EntreeSupp['CP62'] - float(rec['geometry']['coordinates'][2])
elif xxx[-3] == 'Jean-Bernard':
#g['properties']['_CAVE'] = rec['properties']['_STSURVEY'].split('.')[-3]
g['properties']['_CAVE'] = RNames['JB']
g['properties']['_DEPTH'] = EntreeSupp['JB'] - float(rec['geometry']['coordinates'][2])
elif 'A21' in xxx[-4]:
g['properties']['_CAVE'] = RNames['A21']
g['properties']['_DEPTH'] = EntreeSupp['A21'] - float(rec['geometry']['coordinates'][2])
elif 'Mirolda' in xxx[-3]:
g['properties']['_CAVE'] = RNames['Mirolda']
g['properties']['_DEPTH'] = EntreeSupp['Mirolda'] - float(rec['geometry']['coordinates'][2])
else:
g['properties']['_CAVE'] = xxx[-4]
if g['properties']['_CAVE'] == 'A22':
g['properties']['_CAVE'] = 'A(V)22'
#g['properties']['_DEPTH'] = 0
# Trouver l'altitude de l'entrée !!!!
for line in f:
if g['properties']['_CAVE'] in line and line.split('\t')[6] != '\n':
altmax = float(line.split('\t')[6])
g['properties']['_DEPTH'] = altmax - float(rec['geometry']['coordinates'][2])
# Write record
ouput.write (g)
# Update progress bar
bar()
return
#################################################################################################
def shp2gpkg(pathshp, outputspath):
"""
function to convert shp files into gpkg files
Args:
pathshp (str) : path where are stored output shp from Therion
outputspath (str): path where to copy the gpkg files
"""
# files to be converted
files = ['outline2d', 'points2d']
print('shp2gpkg : ', files)
with alive_bar(len(files), title = "\x1b[32;1m- Processing shp2pkg...\x1b[0m", length = 20) as bar:
for fname in files :
if fname == 'walls3d':
print('shp2gpkg does not support walls3d files...\n\t I am only copying the shp file into the right folder')
for ftype in ['.shp', '.dbf', '.prj', '.shx']:
shutil.copy2(pathshp + fname + ftype, outputspath + fname + ftype)
#pass
#input = gpd.read_file(fname + '.shp', layer = 'walls3d', driver = 'ESRI shapefile')
#input.to_file(fname + ".gpkg", driver="GPKG", encoding = 'utf8')
#with fiona.open(fname + '.shp', 'r') as inputshp:
# with fiona.open(fname + '.gpkg', 'w', crs=inputshp.crs, driver='GPKG', schema=inputshp.schema, encoding = 'utf8') as ouput:
# for rec in inputshp:
# # Write record
# ouput.write (g)
else:
input = gpd.read_file(pathshp + fname + '.shp', driver = 'ESRI shapefile')
#input.to_file(fname + ".gpkg", driver="GPKG", encoding = 'utf8')
input.to_file(outputspath + fname + ".gpkg", driver="GPKG")
#input.to_file(fname + ".gpkg", driver="GPKG")
#update bar
bar()
return
#################################################################################################
def ThCutAreas(pathshp, outputspath):
print(' ')
print('****************************************************************')
print('Program to cut areas and lines that are intersecting the outline')
print(' Written by X. Robert, ISTerre')
print(' October 2022 ')
print('****************************************************************')
print(' ')
# Check if areas, lines and outline shapefiles exists...
areaOK = True
for fname in ['outline2d', 'lines2d', 'areas2d', 'points2d']:
if not os.path.isfile(pathshp + fname + '.shp'):
if fname == 'areas2d':
areaOK = False
else:
print(f'\033[91mERROR:\033[00m File {(str(pathshp + fname + '.shp'))} does not exist')
return
# Check if Outputs path exists
if not os.path.exists(outputspath):
print ('\033[91mWARNING:\033[00m ' + outputspath + ' does not exist, I am creating it...')
os.mkdir(outputspath)
#1- Read the outline shapefile
outlines = gpd.read_file(pathshp + 'outline2d.shp', driver = 'ESRI shapefile')
print('Check')
## Change SHP to gpkg
shp2gpkg(pathshp, outputspath)
## Work with points
#AddAltPoint(pathshp, outputspath)
## Work with lines
cutLines(pathshp, outlines, outputspath)
## Work with Areas
if areaOK:
print ('Cuting areas...')
cutareas(pathshp, outlines, outputspath)
else:
print ("No areas to process...")
#5- End ?
print('')
print('Update point, areas and lines done.')
print('')
######################################################################################################
if __name__ == u'__main__':
###################################################
# initiate variables
#inputfile = 'stations3d.shp'
pathshp = './Inputs/'
outputspath = './Outputs/'
###################################################
# Run the transformation
ThCutAreas(pathshp, outputspath)
# End...
+239
View File
@@ -0,0 +1,239 @@
"""
!#############################################################################################
# #
# general_fonctions.py for pythStat.py #
# #
!#############################################################################################
Alex 2026 01 09
"""
import os, logging, sys, re, unicodedata
from pathlib import Path
log = logging.getLogger("Logger")
#################################################################################################
# Couleurs ANSI par niveau de log
#################################################################################################
COLOR_CODES = {
logging.DEBUG: "\033[94m", # Bleu
logging.INFO: "\033[92m", # Vert
logging.WARNING: "\033[95m", # MAGENTA
logging.ERROR: "\033[91m", # Rouge
logging.CRITICAL: "\033[1;91m", # Rouge vif
}
RESET = "\033[0m"
#################################################################################################
# Codes de couleur ANSI
class Colors:
BLACK = '\033[90m'
RED = '\033[91m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
MAGENTA = '\033[95m'
CYAN = '\033[96m'
WHITE = '\033[97m'
ERROR = '\033[91m'
WARNING = '\033[95m'
HEADER = '\033[1;94m'
DEBUG = '\033[94m' # Bleu
INFO = '\033[92m' # Vert
CRITICAL = '\033[1;91m', # Rouge vif
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
#################################################################################################
# Coloration des messages d'aide d'arg #
#################################################################################################
def colored_help(parser):
"""
Affiche l'aide colorée pour les arguments de la ligne de commande.
Args:
parser (argparse.ArgumentParser): Le parseur d'arguments.
Returns:
None
"""
# Captures the help output
help_text = parser.format_help()
# Coloration des différentes parties
colored_help_text = help_text.replace(
'usage:', f'{Colors.ERROR}usage:{Colors.ENDC}'
).replace(
'options:', f'{Colors.GREEN}options:{Colors.ENDC}'
).replace('positional arguments:', f'{Colors.BLUE}positional arguments:{Colors.ENDC}'
).replace(', --help', f'{Colors.BLUE}, --help:{Colors.ENDC}'
).replace('elp:', f'{Colors.BLUE}elp{Colors.ENDC}')
# Surligner les arguments
# for action in parser._actions:
# if action.option_strings:
# # Colorer les options (--xyz)
# for opt in action.option_strings:
# colored_help_text = colored_help_text.replace(opt, f'{Colors.BLUE}{opt}{Colors.ENDC}').replace('--help', f'{Colors.BLUE}--help:{Colors.ENDC}')
# Imprimer le texte coloré
print(colored_help_text)
sys.exit(1)
#################################################################################################
# Mise au format des noms #
#################################################################################################
def sanitize_filename(thName):
"""
Cleans a string to make it compatible with filenames on Windows, Linux, and macOS.
Replaces special and accented characters with compatible characters.
Replaces parentheses with underscores and enforces proper casing.
Args:
thName (str): The filename to clean.
Returns:
str: The cleaned and compatible string.
"""
# Unicode normalization to replace accented characters with their non-accented equivalents
thName = unicodedata.normalize('NFKD', thName).encode('ASCII', 'ignore').decode('ASCII')
# Replace parentheses with underscores
thName = thName.replace('(', '_').replace(')', '_')
# Replace illegal characters with an underscore
thName = re.sub(r'[<>:"/\\|?*\']', '_', thName) # Illegal on Windows
thName = re.sub(r'\s+', '_', thName) # Spaces to underscores
thName = re.sub(r'[^a-zA-Z0-9._-]', '_', thName) # Keep only allowed chars
# Convert to lowercase, then capitalize the first letter
# thName = thName.lower().capitalize()
# thName = thName.capitalize()
# Suppression des underscores en début et fin
thName = thName.strip('_')
return thName or "default_filename" # Avoid empty result
#################################################################################################
# Supprime les codes ANSI (pour l'écriture dans les fichiers)
#################################################################################################
def strip_ansi_codes(text):
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
return ansi_escape.sub('', text)
#################################################################################################
# Formatter pour la console avec couleurs
#################################################################################################
class ConsoleFormatter(logging.Formatter):
def format(self, record):
color = COLOR_CODES.get(record.levelno, "")
message = super().format(record)
return f"{color}{message}{RESET}"
#################################################################################################
# Formatter pour le fichier avec "!!!" sur les erreurs
#################################################################################################
class FileFormatter(logging.Formatter):
def format(self, record):
clean_msg = strip_ansi_codes(record.getMessage())
prefix = "!!! " if record.levelno >= logging.ERROR else ""
record_copy = logging.LogRecord(
name=record.name,
level=record.levelno,
pathname=record.pathname,
lineno=record.lineno,
msg=f"{prefix}{clean_msg}",
args=(),
exc_info=record.exc_info,
func=record.funcName,
sinfo=record.stack_info
)
return super().format(record_copy)
#################################################################################################
# Fonction de configuration du logger
#################################################################################################
def setup_logger(logfile="app.log", debug_log=False):
logger = logging.getLogger("Logger")
logger.setLevel(logging.DEBUG)
logger.handlers.clear()
min_level = logging.DEBUG if debug_log else logging.INFO
# Console stderr handler — affichage à l'écran avec couleurs
stderr_handler = logging.StreamHandler(sys.stderr)
stderr_handler.setLevel(min_level)
stderr_formatter = ConsoleFormatter("%(levelname)s: %(message)s") # <-- Ta classe personnalisée
stderr_handler.setFormatter(stderr_formatter)
logger.addHandler(stderr_handler)
# File handler — fichier de log
file_handler = logging.FileHandler(logfile, encoding="utf-8")
file_handler.setLevel(min_level)
file_formatter = FileFormatter("%(asctime)s - %(levelname)s - %(message)s") # <-- Ta classe personnalisée
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)
return logger
#################################################################################################
# fonction pour réduire l'affichage des chemins long #
#################################################################################################
def safe_relpath(path, base_dir=None, max_depth=3, max_name_len=50, prefix="~"):
"""
Retourne un chemin lisible et sûr pour affichage (logs / UI).
- Compatible Windows / Linux / macOS
- Tronque la profondeur du chemin
- Tronque le nom de fichier si trop long
- Ne lève jamais d'exception
"""
try:
path = Path(path).expanduser().resolve()
except Exception:
return str(path)
try:
base = Path(base_dir).expanduser().resolve() if base_dir else Path.cwd().resolve()
except Exception:
base = None
name = path.name or str(path)
if len(name) > max_name_len:
stem = path.stem[: max(1, max_name_len - 6)]
name = f"{stem}...{path.suffix}"
try:
if base:
rel = path.relative_to(base)
parts = list(rel.parts)
else:
raise ValueError
except Exception:
parts = list(path.parts)
if not parts:
parts = ["."]
if isinstance(max_depth, int) and max_depth > 0 and len(parts) > max_depth:
parts = parts[-max_depth:]
parts.insert(0, prefix)
if parts and parts[-1] not in (".", os.sep):
parts[-1] = name
try:
return os.path.join(*parts)
except Exception:
return name
+17
View File
@@ -0,0 +1,17 @@
"""
!#############################################################################################!
global_data.py for pyThtoQgis.py
!#############################################################################################!
"""
Version = "2026.01.12"
#################################################################################################
pathshp = ".\\Inputs\\"
outputspath = ".\\Outputs\\"
# error_count = 0 # Compteur d'erreurs
debug_log = False
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,15 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"cSpell.words": [
"ENDC",
"geopandas",
"Ggis",
"Qgis"
]
}
}
+665
View File
@@ -0,0 +1,665 @@
######!/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 #
# #
# By Xavier Robert #
# Grenoble, October 2022 #
# #
#############################################################
Written by Xavier Robert, October 2022x
Xavier.robert@ird.fr
Modifié Alex 2025 01 31
Inputs files (16): (.dbf, .prj, .shp, .shx)
- points2d
- lines2d
- areas2d
- outlines
En cas d'erreur corriger manuellement (QGis) les topologies des fichiers
"""
# Do divisions with Reals, not with integers
# Must be at the beginning of the file
from __future__ import division
# Import Python modules
#import numpy as np
import sys, os, argparse, shutil
import matplotlib.pyplot as plt
import tkinter as tk
from tkinter import filedialog
import fiona
from fiona import Env
import shapely
import geopandas as gpd
import pandas as pd
from shapely.geometry import Point, MultiPoint, Polygon, LineString, MultiLineString, MultiPolygon, Polygon
from shapely.geometry import shape, mapping, GeometryCollection
from shapely.ops import transform, unary_union, polygonize
from shapely.errors import TopologicalError
from shapely.validation import make_valid, explain_validity
from collections import Counter
#from functools import wraps
from alive_progress import alive_bar # https://github.com/rsalmei/alive-progress
import Lib.global_data as globalDat
from Lib.general_fonctions import setup_logger, Colors, safe_relpath, colored_help
#################################################################################################
def cutareas(pathshp, outlines, outputspath):
"""
Function to cut shapefiles areas with the outline to only keep the lines inside the outline
Args:
pathshp (str) : path where are stored output shp from Therion
outlines (geopandas obj): the outline shapefile
outputspath (str) : path where to copy the gpkg files
"""
print(f'{Colors.GREEN}Working with :{Colors.ENDC} areas2d.gpkg')
# 2- Validate the outline and Areas shapefile
#for rec in outlines:
# rec2 = validate('outline2d.shp', rec)
# # update correction --> To do ?
# #if rec2 != rec:
#for rec in areas:
# rec2 = validate('areas2d.shp', rec)
# # update correction
# #if rec2 != rec:
# Read the Line Shapefile
# areas = gpd.read_file(pathshp + 'areas2d.shp', driver = 'ESRI shapefile')
areas = gpd.read_file(pathshp + 'areas2d.gpkg')
# Corriger les erreurs de topologie dans les lignes avant traitement
# areas = fix_topology(areas)
# Extract the intersections between outlines and lines
# be careful, for this operation, geopandas needs to work with rtree and not pygeos
# --> uninstall pygeos and install rtree
try:
areasIN = areas.overlay(outlines, how = 'intersection')
except:
print('ERROR: 1) uninstall pygeos and install rtree\n\t2) check your polygons validity')
import rtree
print ('\tYou may check the validity of your polygons with the verify function in QGIS')
areasIN = areas.overlay(outlines, how = 'intersection')
# Removes inner lines that have different id and scrap_id
areasIN = areasIN[areasIN['_SCRAP_ID'] == areasIN ['_ID']]
# Save output
#areasIN.to_file("areas2dMasekd.gpkg", driver = "GPKG", encoding = 'utf8')
areasIN.to_file(outputspath + "areas2dMasekd.gpkg", driver = "GPKG")
return
#################################################################################################
def cutLines(pathshp, outlines, outputspath):
"""
Function to cut shapefiles lines with the outline to only keep the lines inside the outline
Args:
pathshp (str) : path where are stored output shp from Therion
outlines (geopandas obj): the outline shapefile
outputspath (str) : path where to copy the gpkg files
"""
print(f'{Colors.GREEN}Working with :{Colors.ENDC} lines2d.gpkg')
# Read the Line Shapefile
lines = gpd.read_file(pathshp + 'lines2d.gpkg')
# lines = fix_topology(lines)
# Vérifier si outlines est un GeoDataFrame
if not isinstance(outlines, gpd.GeoDataFrame):
print(f"{Colors.GREEN}Outlines is not a GeoDataFrame. Attempting conversion...")
outlines = gpd.read_file(outlines) # Lire un fichier shapefile si outlines est une chaine de caractères
# Extract lines that are not masked by the outline
linesOUT = pd.concat((lines[lines['_TYPE'] == 'centerline'],
lines[lines['_TYPE'] == 'water_flow'],
lines[lines['_TYPE'] == 'label'],
lines[lines['_CLIP'] == 'off']),
ignore_index=True)
# Extract lines will be masked by the outline
linesIN = lines[lines['_CLIP'] != 'off']
linesIN = linesIN[linesIN['_TYPE'] != 'centerline']
linesIN = linesIN[linesIN['_TYPE'] != 'water_flow']
linesIN = linesIN[linesIN['_TYPE'] != 'label']
# Extract the intersections between outlines and lines
# be careful, for this operation, geopandas needs to work with rtree and not pygeos
# --> uninstall pygeos and install rtree
try:
# outlines = outlines.buffer(0) # [Note Alex] Réparer les géométries invalides
linesIN = linesIN.overlay(outlines, how = 'intersection', keep_geom_type=True)
except:
print(f"{Colors.ERROR}ERROR: uninstall pygeos and install rtree\n\t2) check your polygons validity")
print (f"{Colors.ERROR}You may check the validity of your polygons with the verify function in QGIS")
linesIN = linesIN.overlay(outlines, how = 'intersection', keep_geom_type=True)
# Removes inner lines that have different id and scrap_id
linesIN = linesIN[linesIN['_SCRAP_ID'] == linesIN ['_ID']]
# Merge the IN and OUT database
linesTOT = pd.concat((linesOUT, linesIN),ignore_index=True)
# Save output
linesTOT.to_file(outputspath + "lines2dMasekd.gpkg", driver="GPKG")
return
#################################################################################################
def shp2gpkg(pathshp, infile, outputspath, outfile ):
"""
Function to convert shp files into gpkg files using Fiona.
Args:
pathshp (str): Path where the input shp files are stored.
infile (str): Name of the file to be converted (without extension).
outputspath (str): Path where the output gpkg files will be saved.
outfile (str): Name of the output file (without extension).
"""
try:
# Configuration de l'environnement Fiona pour accepter les géométries non fermées
with Env(OGR_GEOMETRY_ACCEPT_UNCLOSED_RING="YES"):
input_shp = os.path.join(pathshp, infile + '.shp')
output_gpkg = os.path.join(outputspath, outfile + '.gpkg')
# Vérification que le fichier source existe
if not os.path.exists(input_shp):
raise FileNotFoundError(f"\t{Colors.ERROR}Error (shp2gpkg): the file {Colors.ENDC}{input_shp}{Colors.ERROR} did not exist.")
# Lecture du fichier Shapefile
with fiona.open(input_shp, 'r', encoding='utf-8') as source:
geom_types = Counter() # pour compter le nombre de chaque type
has_z = set()
for feat in source:
geom_type = feat["geometry"]["type"]
geom_types[geom_type] += 1
coords = feat["geometry"]["coordinates"]
# Vérifie si la géométrie a un Z
def check_z(coords):
"""
Détecte la présence d'une coordonnée Z
quelle que soit la profondeur de la géométrie
"""
if isinstance(coords, (list, tuple)):
# Cas Point : (x,y) ou (x,y,z)
if len(coords) in (2, 3) and all(isinstance(c, (int, float)) for c in coords):
return len(coords) == 3
# Cas Line / Polygon / Multi*
for c in coords:
if check_z(c):
return True
return False
if check_z(coords):
has_z.add(True)
else:
has_z.add(False)
print(
f"{Colors.GREEN}File conversion to GPKG: {Colors.ENDC}{input_shp}"
f"{Colors.GREEN}, geometries found: {Colors.ENDC}{dict(geom_types)}"
f"{Colors.GREEN}, with altitude: {Colors.ENDC}{has_z}"
)
# Affichage du nombre d'objets et de leur type
num_features = len(source)
geometry_type = source.schema['geometry']
# Vérification que le driver GPKG est disponible
if 'GPKG' not in fiona.supported_drivers:
raise RuntimeError(f"{Colors.ERROR}Error: The GPKG driver is not supported by Fiona{Colors.ENDC}")
# Création du fichier GeoPackage
with fiona.open( output_gpkg, 'w', driver='GPKG', schema=source.schema, crs=source.crs, encoding='utf-8') as destination:
for feature in source:
destination.write(feature)
print(f'{Colors.GREEN}Conversion to GPKG OK, {Colors.GREEN} file : {Colors.ENDC}{pathshp}{infile}.shp{Colors.GREEN} to : {Colors.ENDC}{outputspath}{outfile}.gpkg, type {geometry_type} : {num_features}')
except FileNotFoundError as e:
print(f"{Colors.ERROR}Error (shp2gpkg): {Colors.ENDC}{e}", file=sys.stderr)
except RuntimeError as e:
print(f"{Colors.ERROR}Error (shp2gpkg): {Colors.ENDC}{e}", file=sys.stderr)
except fiona.errors.FionaError as e:
print(f"{Colors.ERROR}Error read/write file (shp2gpkg): {Colors.ENDC}{e}", file=sys.stderr)
except Exception as e:
print(f"{Colors.ERROR}Error unknown (shp2gpkg): {Colors.ENDC}{e}", file=sys.stderr)
#################################################################################################
def poly_rock(infile, outfile):
"""
Converts line features from the input GeoPackage file into closed polygons,
and saves the valid ones to an existing layer in the output GeoPackage.
Only features with the attribute _Type equal to 'rock-edge' or 'rock-border' are considered.
"""
# Load the input layer (assumed to be a line layer)
gdf = gpd.read_file(infile, encoding='utf-8') # Adjust layer name if needed
# Filter features based on _Type attribute
gdf = gdf[gdf['_TYPE'].isin(['rock-edge', 'rock-border'])]
# Attempt to convert LineStrings to Polygons
polygons = []
for _, row in gdf.iterrows():
geom = row.geometry
if isinstance(geom, LineString) and geom.is_ring:
new_row = row.copy()
new_row['geometry'] = Polygon(geom)
polygons.append(new_row)
# Create a GeoDataFrame from the valid polygons
if polygons:
poly_gdf = gpd.GeoDataFrame(polygons, geometry='geometry', crs=gdf.crs)
# Load existing output data if it exists
try:
existing_gdf = gpd.read_file(outfile, encoding='utf-8')
poly_gdf = gpd.pd.concat([existing_gdf, poly_gdf], ignore_index=True)
except Exception:
pass # If file doesn't exist, create a new one
# Save updated data to the output GeoPackage
poly_gdf.to_file(outfile, driver='GPKG', encoding='utf-8') # Adjust layer name if needed
print(f"{Colors.GREEN}Added {Colors.ENDC}{len(polygons)} {Colors.GREEN}polygons to {Colors.ENDC}{outfile}.")
else:
print(f"{Colors.ERROR}No valid closed polylines found.")
#################################################################################################
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):
print(f"{Colors.ERROR}File not found: {Colors.ENDC}{file_path}")
return {}, -1
with fiona.open(file_path, "r") as src:
for i, record in enumerate(src):
total_records += 1
# Vérifier si la géométrie est présente
if record is None or record.get('geometry') is None:
props = record.get('properties', {})
print(f"{Colors.ERROR}Error in file {Colors.ENDC}{safe_relpath(file_path)}{Colors.ERROR}, record {Colors.ENDC}{i+1}{Colors.ERROR} has no geometry, correct it : "
f"_ID: {Colors.ENDC}{props.get('_ID')}{Colors.ERROR}, _NAME: {Colors.ENDC}{props.get('_NAME')}{Colors.ERROR}, _SURVEY: {Colors.ENDC}{props.get('_SURVEY')}{Colors.ENDC}")
continue
# Créer l'objet Shapely
try:
geometry = shape(record['geometry'])
except Exception as e:
props = record.get('properties', {})
print(f"{Colors.ERROR}Error in file {Colors.ENDC}{safe_relpath(file_path)}{Colors.ERROR},Cannot create shape for record {Colors.ENDC}{i+1}{Colors.ERROR}: {Colors.ENDC}{e}{Colors.ERROR}"
f"_ID: {Colors.ENDC}{props.get('_ID')}{Colors.ERROR}, _NAME: {Colors.ENDC}{props.get('_NAME')}{Colors.ERROR}, _SURVEY: {Colors.ENDC}{props.get('_SURVEY')}{Colors.ENDC}")
continue
# Ignorer les géométries vides
if geometry.is_empty:
print(f"{Colors.WARNING}Warning, file {Colors.ENDC}{safe_relpath(file_path)}{Colors.WARNING},Record {i+1} has empty geometry. Skipping.{Colors.ENDC}")
continue
# Comptage des types de géométrie
geom_type = geometry.geom_type
record_types[geom_type] = record_types.get(geom_type, 0) + 1
# Vérifier la validité topologique
try:
validity_explanation = explain_validity(geometry)
if validity_explanation != "Valid Geometry":
total_errors += 1
# Conserver l'explication complète comme type d'erreur
error_details.setdefault(validity_explanation, []).append(i)
except Exception as e:
print(f"{Colors.ERROR}Error in file {Colors.ENDC}{safe_relpath(file_path)}{Colors.ERROR}, validating geometry for record {Colors.ENDC}{i+1}{Colors.ERROR}: {Colors.ENDC}{e}{Colors.ENDC}")
print(f"{Colors.GREEN}Geometry num: {Colors.ENDC}{i+1}{Colors.YELLOW}, types found: {Colors.ENDC}{record_types}")
# Affichage du résumé
if total_errors == 0:
print(f"{Colors.GREEN}File error check OK: {Colors.ENDC}{safe_relpath(file_path)}{Colors.GREEN}, "
f"records: {Colors.ENDC}{total_records}{Colors.GREEN}, no errors found")
else:
print(f"{Colors.ERROR}File error check NOK: {Colors.ENDC}{safe_relpath(file_path)}{Colors.ERROR}, "
f"records: {Colors.ENDC}{total_records}{Colors.ERROR}, total errors: {Colors.ENDC}{total_errors}")
# for err_type, indices in error_details.items():
# print(f"{Colors.ERROR}Detail error in file {Colors.ENDC}{safe_relpath(file_path)}{Colors.ERROR}, {Colors.ENDC}{err_type} "
# f"{Colors.ERROR}occurrences: {Colors.ENDC}{len(indices)}{Colors.ENDC}")
# Optionnel : afficher le détail des types de géométrie
print(f"{Colors.GREEN}Geometry in file: {Colors.ENDC}{safe_relpath(file_path)}{Colors.GREEN}, types found: {Colors.ENDC}{record_types}")
return error_details, total_errors
except Exception as e:
print(f"{Colors.ERROR}Topology error when analyzing the shapefile: {Colors.ENDC}"
f"{safe_relpath(file_path)}{Colors.ERROR}, code: {Colors.ENDC}{e}")
return {}, -1
#################################################################################################
def fix_geometries(input_shp, output_shp):
"""
Fixes geometry errors in a Shapefile and saves only objects of the same type as the source file.
Displays a summary of the modifications and a report of geometries by type before and after processing.
:param input_shp: Path to the input Shapefile
:param output_shp: Path to the output Shapefile
"""
try :
with fiona.open(input_shp, 'r') as src:
meta = src.meta # File metadata
original_geom_type = meta['schema']['geometry'].upper() # Expected geometry type (in uppercase to avoid format discrepancies)
original_geom_type_simple = original_geom_type.replace('3D ', '') # Remove '3D ' prefix
fixed_features = []
geom_counts_before = Counter()
geom_counts_after = Counter()
modifications = 0
error_details = {}
corrected = 0
i = 0
for feature in src:
fixed_feature = dict(feature) # Copy the feature
geom = shape(feature['geometry'])
geom_type = geom.geom_type.upper()
geom_counts_before[geom_type] += 1
# Fix the geometry
valid_geom = make_valid(geom)
geom_type_fixed = valid_geom.geom_type.upper()
geom_counts_after[geom_type_fixed] += 1
# Check if the fixed geometry is of the same type as the original
if geom_type_fixed == original_geom_type or geom_type_fixed == original_geom_type_simple:
fixed_feature['geometry'] = mapping(valid_geom)
fixed_features.append(fixed_feature)
else:
modifications += 1
try:
# Validate the geometry and explain any issues
validity_explanation = explain_validity(geom)
if validity_explanation != "Valid Geometry":
# Extract the type of error
error_type = validity_explanation.split(" ")[0] # First word of the explanation
# Add the record index to the error details
if error_type in error_details:
error_details[error_type].append(i)
else:
error_details[error_type] = [i]
if error_type=="Too" : corrected += 1
i += 1
else :
props = feature.get('properties', {})
print(f"{Colors.ERROR}Error in file {Colors.ENDC}{safe_relpath(input_shp)}{Colors.ERROR},correct it manually, "
f"_ID: {Colors.ENDC}{props.get('_ID')}{Colors.ERROR}, _NAME: {Colors.ENDC}{props.get('_NAME')}{Colors.ERROR}, _SURVEY: {Colors.ENDC}{props.get('_SURVEY')}{Colors.ENDC}")
except Exception as e:
print(f"{Colors.ERROR}Error processing record {Colors.ENDC}: {e}")
# Write the output file with only geometries of the original type
if fixed_features:
with fiona.open(output_shp, 'w', **meta) as dst:
dst.writerecords(fixed_features)
# print(f"{Colors.GREEN}Correction completed{Colors.GREEN}, file {Colors.ENDC}{input_shp}{Colors.GREEN} saved as: {Colors.ENDC}{output_shp}")
if error_details:
for error_type, indices in error_details.items():
if error_type=="Too" :
print(f"{Colors.GREEN}Correction completed{Colors.GREEN}, file {Colors.ENDC}{input_shp}{Colors.GREEN} saved as: {Colors.ENDC}{output_shp}{Colors.GREEN} Erreur type : {Colors.ENDC}{error_type} : {len(indices)}{Colors.GREEN} occurrences corrected{Colors.ENDC}")
else :
print(f"{Colors.WARNING}Correction issue{Colors.ERROR}, file {Colors.ENDC}{input_shp}{Colors.ERROR} saved as: {Colors.ENDC}{output_shp}{Colors.ERROR} Erreur type : {Colors.ENDC}{error_type} : {len(indices)}{Colors.ERROR} occurrences{Colors.ENDC}")
else:
print(f"{Colors.ERROR}Error: No valid features found in {Colors.ENDC}{input_shp}.{Colors.ERROR}No file generated.{Colors.ENDC}")
# Display the summary
if modifications !=0 : print(f"{Colors.WARNING}Total number of geometries ignored: {Colors.ENDC}{modifications}")
print(f"{Colors.INFO}Total number of geometries corrected : {Colors.ENDC}{corrected}")
print(f"{Colors.INFO}Before correction: {Colors.ENDC}{dict(geom_counts_before)}")
print(f"{Colors.INFO}After correction: {Colors.ENDC}{dict(geom_counts_after)}")
# Return the number of modifications
return modifications - corrected
except Exception as e:
print(f"{Colors.ERROR}Fix geometry error in the shapefile: {Colors.ENDC}"
f"{safe_relpath(input_shp)}{Colors.ERROR}, code: {Colors.ENDC}{e}")
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):
print (f"{Colors.WARNING}WARNING: {Colors.ENDC}{safe_relpath(outputspath)}{Colors.WARNING} does not exist, I am creating it...")
os.mkdir(outputspath)
modifications = 0
if os.path.isfile(pathshp + 'areas2d.shp') :
file_list = ['outline2d', 'lines2d', 'areas2d', 'points2d']
areaOK = True
else :
file_list = ['outline2d', 'lines2d', 'points2d']
areaOK = False
print(f"{Colors.HEADER}{Colors.UNDERLINE}Step 1: Test files and convert to GPKG format in the folder:{Colors.ENDC} {safe_relpath(outputspath)}")
for fname in file_list:
print(f"{Colors.HEADER}Working with file: {Colors.ENDC}{fname}.shp")
if not os.path.isfile(pathshp + fname + '.shp'):
if fname == 'areas2d':
areaOK = False
else:
print(f"{Colors.ERROR}ERROR the file {Colors.ENDC}{(str(pathshp + fname + '.shp'))}{Colors.ERROR} does not exist'{Colors.ENDC}")
return False
err = count_topology_errors(pathshp + fname + '.shp')
if err[1] == -1 : return False
if err[1] != 0 :
modifications += fix_geometries(pathshp + fname + '.shp', pathshp + fname + '_fixed.shp')
err2 = count_topology_errors(pathshp + fname + '_fixed.shp')
if err2[1] == -1 : return False
if err2[1] == 0 : shp2gpkg(pathshp, fname + "_fixed", outputspath, fname)
else :
print(f'{Colors.ERROR}ERROR: in file {Colors.ENDC}{(str(pathshp + fname + '.shp'))} {Colors.ERROR} please fix it manually with QGis... {Colors.ENDC}')
return False
else :
shp2gpkg(pathshp, fname, outputspath, fname)
print(f"{Colors.HEADER}{Colors.UNDERLINE}Step 2: Adapte files for Qgis in the folder:{Colors.ENDC} {safe_relpath(outputspath)}")
#1- Read the outline shapefile
outlines = gpd.read_file(outputspath + 'outline2d.gpkg')
## Work with lines
cutLines(outputspath, outlines, outputspath)
## Work with Areas
if areaOK == True :
cutareas(outputspath, outlines, outputspath)
poly_rock(outputspath + "lines2d.gpkg", outputspath + "areas2dMasekd.gpkg")
os.remove(outputspath + "areas2d.gpkg")
os.remove(outputspath + "lines2d.gpkg")
if modifications == 0 :
print(f'{Colors.GREEN}Update point, areas and lines done without error {Colors.ENDC}')
else :
print(f'{Colors.GREEN}Update point, areas and lines done with warning {Colors.ENDC}{modifications}{Colors.GREEN} to be checked{Colors.ENDC}')
else :
os.remove(outputspath + "lines2d.gpkg")
if modifications == 0 :
print(f'{Colors.HEADER}Update point and lines done without error {Colors.ENDC}')
else :
print(f'{Colors.GREEN}Update point and lines done with warning {Colors.ENDC}{modifications}{Colors.GREEN} to be checked{Colors.ENDC}')
#####################################################################################################################################
# #
# Main #
# #
#####################################################################################################################################
if __name__ == u'__main__':
###################################################
#################################################################################################
# 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 for the input folder\n"
f"test\t-> Tests fonction (debug)\n"
)
)
parser.epilog = (
f"{Colors.HEADER}to generate shp files with therion, add in .thconfig : "
f"-> {Colors.ENDC}export model -fmt esri -o Outputs/SHP/ -enc UTF-8"
)
# 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)
print(f'{Colors.HEADER}*********************************************************************************************************')
print(f'{Colors.HEADER}Script to generate QGis (.gpkg) files from Therion (.shp) files with auto-correction if possible')
print(f'{Colors.HEADER} Original written by X. Robert, ISTerre : {Colors.ENDC}October 2022')
print(f'{Colors.HEADER} Updated by : {Colors.ENDC}alexandre.pont@yahoo.fr')
print(f'{Colors.HEADER} Version : {Colors.ENDC}{globalDat.Version}')
if args.option == "auto" :
print(f'{Colors.HEADER} input folder : {Colors.ENDC}{globalDat.pathshp}')
print(f'{Colors.HEADER} output folder : {Colors.ENDC}{globalDat.outputspath}')
print(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")
if not input_folder_name:
print(f"{Colors.ERROR}No folder selected. The program will terminate")
sys.exit()
input_folder = input_folder_name + "\\"
print(f'{Colors.HEADER} input folder : {Colors.ENDC}{safe_relpath(input_folder)}')
print(f'{Colors.HEADER} output folder : {Colors.ENDC}{globalDat.outputspath}')
print(f'{Colors.HEADER}****************************************************************')
ThtoQGis(input_folder, globalDat.outputspath)
elif args.option == "test" :
exit(1)