crs-zone-toolkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crs_zone_toolkit/__init__.py +178 -0
- crs_zone_toolkit/affichage.py +220 -0
- crs_zone_toolkit/cli.py +385 -0
- crs_zone_toolkit/core/__init__.py +5 -0
- crs_zone_toolkit/core/analysis.py +544 -0
- crs_zone_toolkit/core/apply.py +315 -0
- crs_zone_toolkit/core/decoupage.py +104 -0
- crs_zone_toolkit/core/errors.py +46 -0
- crs_zone_toolkit/core/gridgen.py +63 -0
- crs_zone_toolkit/core/messages.py +762 -0
- crs_zone_toolkit/core/profile.py +67 -0
- crs_zone_toolkit/core/report.py +291 -0
- crs_zone_toolkit/core/results.py +161 -0
- crs_zone_toolkit/core/targets.py +47 -0
- crs_zone_toolkit/py.typed +0 -0
- crs_zone_toolkit/regions/__init__.py +4 -0
- crs_zone_toolkit/regions/loader.py +256 -0
- crs_zone_toolkit/regions/qc/grille_mtm_qc.geojson +16 -0
- crs_zone_toolkit/regions/qc/limite_qc.geojson +8 -0
- crs_zone_toolkit/regions/qc/profil.toml +135 -0
- crs_zone_toolkit/templates/rapport.html.j2 +302 -0
- crs_zone_toolkit-0.1.0.dist-info/METADATA +279 -0
- crs_zone_toolkit-0.1.0.dist-info/RECORD +26 -0
- crs_zone_toolkit-0.1.0.dist-info/WHEEL +4 -0
- crs_zone_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
- crs_zone_toolkit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""crs-zone-toolkit — analyse, recommandation et reprojection CRS (profils régionaux, V1 : Québec).
|
|
2
|
+
|
|
3
|
+
API publique (contrat : docs/ARCHITECTURE.md §4) :
|
|
4
|
+
from crs_zone_toolkit import analyze, apply, report
|
|
5
|
+
`analyze` (recommandation CRS), `apply` (exécution d'une décision) et
|
|
6
|
+
`report` (rapport HTML d'analyse) sont implémentées et exportées.
|
|
7
|
+
`generate_grid` (génération de grille MTM) reste un outil de développement
|
|
8
|
+
interne (docs/gridgen), pas encore exposé ici.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from crs_zone_toolkit.core.results import AnalysisResult, ApplyResult, Decision
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
import geopandas as gpd
|
|
20
|
+
|
|
21
|
+
from crs_zone_toolkit.core.profile import RegionProfile
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0"
|
|
24
|
+
|
|
25
|
+
_FORMATS_GRILLE: dict[str, str] = {"geojson": "GeoJSON", "gpkg": "GPKG"}
|
|
26
|
+
FORMATS_GRILLE: tuple[str, ...] = tuple(sorted(_FORMATS_GRILLE))
|
|
27
|
+
"""Formats d'écriture de la grille (SPEC §6 : geojson | gpkg — pas de shapefile)."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _prepare_source_layer(
|
|
31
|
+
layer: gpd.GeoDataFrame, assume_crs: str | int | None
|
|
32
|
+
) -> gpd.GeoDataFrame:
|
|
33
|
+
"""Assigne le CRS supposé à la couche si elle n'en déclare pas (dette J4 #1).
|
|
34
|
+
|
|
35
|
+
N'ASSIGNE pas de reprojection : pose seulement l'étiquette CRS quand la source
|
|
36
|
+
est muette et qu'un --assume-crs est fourni. Partagé par apply/report/CLI.
|
|
37
|
+
"""
|
|
38
|
+
if layer.crs is None and assume_crs is not None:
|
|
39
|
+
from pyproj import CRS
|
|
40
|
+
|
|
41
|
+
return layer.set_crs(CRS.from_user_input(assume_crs))
|
|
42
|
+
return layer
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _charger_et_analyser(
|
|
46
|
+
source: Path | str,
|
|
47
|
+
*,
|
|
48
|
+
region: str = "qc",
|
|
49
|
+
assume_crs: str | int | None = None,
|
|
50
|
+
n_samples: int | None = None,
|
|
51
|
+
) -> tuple[gpd.GeoDataFrame, AnalysisResult, RegionProfile, gpd.GeoDataFrame]:
|
|
52
|
+
"""Composition partagée : lit et analyse la couche UNE fois (SPEC §6).
|
|
53
|
+
|
|
54
|
+
Ordre voulu : analyse AVANT assignation du CRS supposé, pour que
|
|
55
|
+
l'avertissement « CRS supposé » soit capté par le moteur (SPEC §4.2.2).
|
|
56
|
+
Renvoie la couche prête pour l'écriture (CRS assigné) + le résultat + le
|
|
57
|
+
profil + la grille, réutilisables par apply/report/CLI sans re-lecture.
|
|
58
|
+
"""
|
|
59
|
+
import geopandas as gpd
|
|
60
|
+
|
|
61
|
+
from crs_zone_toolkit.core import analysis as _analysis
|
|
62
|
+
from crs_zone_toolkit.core import messages as _msg
|
|
63
|
+
from crs_zone_toolkit.core.errors import LayerReadError
|
|
64
|
+
from crs_zone_toolkit.regions.loader import load_grid, load_profile
|
|
65
|
+
|
|
66
|
+
source = Path(source)
|
|
67
|
+
profile = load_profile(region)
|
|
68
|
+
grid = load_grid(profile)
|
|
69
|
+
if not source.is_file():
|
|
70
|
+
raise LayerReadError(_msg.fichier_introuvable(str(source)))
|
|
71
|
+
try:
|
|
72
|
+
layer = gpd.read_file(source)
|
|
73
|
+
except Exception as exc: # lecture géospatiale en échec (SPEC §10, code 1)
|
|
74
|
+
raise LayerReadError(_msg.format_non_supporte(str(source))) from exc
|
|
75
|
+
result = _analysis.analyze(
|
|
76
|
+
layer, source.stem, profile=profile, grid=grid, assume_crs=assume_crs, n_samples=n_samples
|
|
77
|
+
)
|
|
78
|
+
layer = _prepare_source_layer(layer, assume_crs)
|
|
79
|
+
return layer, result, profile, grid
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def analyze(
|
|
83
|
+
source: Path | str,
|
|
84
|
+
*,
|
|
85
|
+
region: str = "qc",
|
|
86
|
+
assume_crs: str | int | None = None,
|
|
87
|
+
n_samples: int | None = None,
|
|
88
|
+
) -> AnalysisResult:
|
|
89
|
+
"""Analyse une couche (contrat ARCHITECTURE §4)."""
|
|
90
|
+
_, result, _, _ = _charger_et_analyser(
|
|
91
|
+
source, region=region, assume_crs=assume_crs, n_samples=n_samples
|
|
92
|
+
)
|
|
93
|
+
return result
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def apply(
|
|
97
|
+
source: Path | str,
|
|
98
|
+
decision: Decision,
|
|
99
|
+
*,
|
|
100
|
+
region: str = "qc",
|
|
101
|
+
out_dir: Path | str | None = None,
|
|
102
|
+
out_format: str = "gpkg",
|
|
103
|
+
overwrite: bool = False,
|
|
104
|
+
assume_crs: str | int | None = None,
|
|
105
|
+
) -> ApplyResult:
|
|
106
|
+
"""Exécute une décision sur une couche (contrat ARCHITECTURE §4)."""
|
|
107
|
+
from crs_zone_toolkit.core import apply as _apply
|
|
108
|
+
|
|
109
|
+
source = Path(source)
|
|
110
|
+
layer, result, profile, grid = _charger_et_analyser(
|
|
111
|
+
source, region=region, assume_crs=assume_crs
|
|
112
|
+
)
|
|
113
|
+
cible = Path(out_dir) if out_dir is not None else source.parent
|
|
114
|
+
return _apply.apply(
|
|
115
|
+
layer,
|
|
116
|
+
source.stem,
|
|
117
|
+
result,
|
|
118
|
+
decision,
|
|
119
|
+
profile=profile,
|
|
120
|
+
grid=grid,
|
|
121
|
+
out_dir=cible,
|
|
122
|
+
out_format=out_format,
|
|
123
|
+
overwrite=overwrite,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _generer_grille(
|
|
128
|
+
*,
|
|
129
|
+
region: str = "qc",
|
|
130
|
+
out: Path | str,
|
|
131
|
+
out_format: str = "geojson",
|
|
132
|
+
clip: bool = True,
|
|
133
|
+
) -> tuple[Path, int, tuple[str, ...]]:
|
|
134
|
+
"""Compose loader + gridgen + écriture ; renvoie (chemin, n_entités, attributs)."""
|
|
135
|
+
if out_format not in _FORMATS_GRILLE: # DT-19 : même garde que apply (DT-06)
|
|
136
|
+
from crs_zone_toolkit.core import messages as _m
|
|
137
|
+
|
|
138
|
+
raise ValueError(_m.format_sortie_invalide(out_format, list(FORMATS_GRILLE)))
|
|
139
|
+
|
|
140
|
+
from crs_zone_toolkit.core.gridgen import _ATTRIBUTS, build_grid
|
|
141
|
+
from crs_zone_toolkit.regions.loader import load_boundary, load_profile
|
|
142
|
+
|
|
143
|
+
profile = load_profile(region)
|
|
144
|
+
boundary = load_boundary(profile)
|
|
145
|
+
grille = build_grid(profile, boundary, clip=clip)
|
|
146
|
+
out = Path(out)
|
|
147
|
+
if out.parent != Path():
|
|
148
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
grille.to_file(out, driver=_FORMATS_GRILLE[out_format])
|
|
150
|
+
return out, len(grille), _ATTRIBUTS
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def report(
|
|
154
|
+
source: Path | str,
|
|
155
|
+
*,
|
|
156
|
+
region: str = "qc",
|
|
157
|
+
out_dir: Path | str | None = None,
|
|
158
|
+
assume_crs: str | int | None = None,
|
|
159
|
+
overwrite: bool = False,
|
|
160
|
+
) -> Path:
|
|
161
|
+
"""Génère le rapport HTML d'analyse d'une couche (contrat SPEC §7)."""
|
|
162
|
+
from datetime import UTC, datetime
|
|
163
|
+
|
|
164
|
+
from crs_zone_toolkit.core import report as _report
|
|
165
|
+
|
|
166
|
+
source = Path(source)
|
|
167
|
+
layer, result, profile, grid = _charger_et_analyser(
|
|
168
|
+
source, region=region, assume_crs=assume_crs
|
|
169
|
+
)
|
|
170
|
+
quand = datetime.now(UTC)
|
|
171
|
+
html = _report.render_html(
|
|
172
|
+
result, layer, profile=profile, grid=grid, generated_at=quand, fichier=source.name
|
|
173
|
+
)
|
|
174
|
+
cible = Path(out_dir) if out_dir is not None else source.parent
|
|
175
|
+
return _report._ecrire(html, source, out_dir=cible, overwrite=overwrite, generated_at=quand)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
__all__ = ["analyze", "apply", "report", "AnalysisResult", "ApplyResult", "Decision"]
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Rendu Rich des écrans CLI (aucune logique métier, présentation seule).
|
|
2
|
+
|
|
3
|
+
Toutes les chaînes proviennent de core.messages ; ce module ne fait que les
|
|
4
|
+
disposer avec Rich. Les maquettes exactes sont dans docs/CLI_UX.md.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.padding import Padding
|
|
13
|
+
from rich.rule import Rule
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
from rich.text import Text
|
|
16
|
+
|
|
17
|
+
import crs_zone_toolkit
|
|
18
|
+
from crs_zone_toolkit.core import messages as msg
|
|
19
|
+
from crs_zone_toolkit.core.profile import RegionProfile
|
|
20
|
+
from crs_zone_toolkit.core.results import AnalysisResult, ApplyResult
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _ligne(console: Console, texte: str, *, tete: str = "", glyphe: str = "") -> None:
|
|
24
|
+
"""Imprime une ligne dont les retours s'alignent SOUS le texte (N18, DT-29).
|
|
25
|
+
|
|
26
|
+
`tete` est le préfixe en clair (« ␣␣ », « ␣␣⚠␣ »…) ; `glyphe` est son
|
|
27
|
+
équivalent balisé Rich, appliqué à la seule première ligne. Le découpage
|
|
28
|
+
lui-même vit dans `messages.envelopper` : il est ainsi testable comme une
|
|
29
|
+
fonction pure, sans passer par une console (TEST_PLAN §5).
|
|
30
|
+
"""
|
|
31
|
+
lignes = msg.envelopper(texte, largeur=console.width, tete=tete)
|
|
32
|
+
premiere = lignes[0]
|
|
33
|
+
console.print(f"{glyphe}{premiere[len(tete) :]}" if glyphe else premiere)
|
|
34
|
+
for suite in lignes[1:]:
|
|
35
|
+
if len(suite) <= console.width:
|
|
36
|
+
console.print(suite)
|
|
37
|
+
continue
|
|
38
|
+
# Jeton insécable plus long que la console (un chemin de fichier) :
|
|
39
|
+
# `envelopper` le laisse entier exprès : coupé, il n'est plus copiable.
|
|
40
|
+
# Rich le coupera donc quand même, mais `Padding` lui fait garder le
|
|
41
|
+
# retrait au lieu de le renvoyer en colonne 0, qui est tout le sujet de N18.
|
|
42
|
+
console.print(Padding(Text(suite.lstrip()), (0, 0, 0, len(tete))))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def message(console: Console, texte: str) -> None:
|
|
46
|
+
"""Ligne d'information simple, enveloppée sans débordement (N18)."""
|
|
47
|
+
_ligne(console, texte)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def erreur(console: Console, texte: str) -> None:
|
|
51
|
+
"""Ligne d'erreur « ✗ … », nommée par N18, qui l'avait relevée débordante."""
|
|
52
|
+
_ligne(console, texte, tete="✗ ", glyphe="[red]✗[/red] ")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def resume_grille(
|
|
56
|
+
console: Console, chemin: Path, n: int, attributs: tuple[str, ...], *, clip: bool
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Écran de la commande grid (CLI_UX §7)."""
|
|
59
|
+
console.print(msg.grille_entete())
|
|
60
|
+
console.print(msg.grille_ligne_decoupe(clip))
|
|
61
|
+
console.print(f"[green]✓[/green] {msg.grille_ligne_ecrite(str(chemin), n, attributs)}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def bandeau_assume_crs(console: Console, code: str) -> None:
|
|
65
|
+
"""Bandeau permanent « CRS supposé » (CLI_UX §6.2, 2 lignes)."""
|
|
66
|
+
_ligne(console, msg.assume_crs_bandeau(code), tete="⚠ ", glyphe="[yellow]⚠[/yellow] ")
|
|
67
|
+
_ligne(console, msg.ASSUME_CRS_TRACE, tete=" ")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def resume_analyse(
|
|
71
|
+
console: Console,
|
|
72
|
+
result: AnalysisResult,
|
|
73
|
+
chemin_rapport: Path | None,
|
|
74
|
+
*,
|
|
75
|
+
couche: Path,
|
|
76
|
+
n_entites: int,
|
|
77
|
+
profile: RegionProfile,
|
|
78
|
+
crs_geographique: bool,
|
|
79
|
+
famille_cible: str,
|
|
80
|
+
abrege: bool = False,
|
|
81
|
+
suggerer_apply: bool = True,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Résumé terminal d'une analyse (CLI_UX §2/§3/§5).
|
|
84
|
+
|
|
85
|
+
`chemin_rapport` est optionnel : `apply` n'écrit aucun rapport HTML (seule `analyze`
|
|
86
|
+
en écrit un, cf. `cli.py`) ; quand il vaut `None`, la ligne « Rapport détaillé » est
|
|
87
|
+
omise (DT-20 n°5) ; la ligne « Pour appliquer » qui suit reste affichée.
|
|
88
|
+
|
|
89
|
+
`suggerer_apply` : la ligne « Pour appliquer : crszone apply … » ne s'adresse
|
|
90
|
+
qu'au lecteur d'une **analyse**. `apply` la passe à False : elle y proposait la
|
|
91
|
+
commande en cours (N2, DT-29).
|
|
92
|
+
|
|
93
|
+
`famille_cible` (« csrs »/« nad83 ») vient de l'appelant (`target_family(result.famille)`,
|
|
94
|
+
importé localement dans `cli.py`) : ce module ne doit charger ni `geopandas` ni `pyproj`
|
|
95
|
+
(finitions revue B (e)) ; seul le libellé est résolu ici, via `msg.FAMILLE_LIBELLE`.
|
|
96
|
+
"""
|
|
97
|
+
reco = result.recommandation
|
|
98
|
+
entete = Table.grid(expand=True) # titre à gauche, version à droite (CLI_UX §2)
|
|
99
|
+
entete.add_column(justify="left")
|
|
100
|
+
entete.add_column(justify="right")
|
|
101
|
+
entete.add_row(
|
|
102
|
+
f"[bold]{msg.analyse_entete(profile.nom, profile.id)}[/bold]",
|
|
103
|
+
msg.analyse_version(crs_zone_toolkit.__version__),
|
|
104
|
+
)
|
|
105
|
+
console.print(entete)
|
|
106
|
+
console.print(Rule(style="dim"))
|
|
107
|
+
_ligne(console, msg.analyse_ligne_couche(couche.name, result.type_geometrie, n_entites))
|
|
108
|
+
crs = result.crs_entree
|
|
109
|
+
_ligne(
|
|
110
|
+
console,
|
|
111
|
+
msg.analyse_ligne_crs_declare(
|
|
112
|
+
crs.get("epsg"), str(crs.get("etiquette", "")), geographique=crs_geographique
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
emprise = result.emprise
|
|
116
|
+
console.print(
|
|
117
|
+
msg.analyse_ligne_emprise(
|
|
118
|
+
emprise.lon_min, emprise.lat_min, emprise.lon_max, emprise.lat_max
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
if not abrege:
|
|
122
|
+
console.print()
|
|
123
|
+
console.print(msg.analyse_repartition_titre(result.type_geometrie))
|
|
124
|
+
mc_par_zone = {f.zone: f.meridien_central for f in profile.fuseaux}
|
|
125
|
+
if result.zones_traversees:
|
|
126
|
+
for ligne in msg.analyse_bloc_fuseaux(
|
|
127
|
+
[(zp.zone, zp.part * 100, mc_par_zone[zp.zone]) for zp in result.zones_traversees]
|
|
128
|
+
):
|
|
129
|
+
console.print(ligne)
|
|
130
|
+
else: # 100 % hors profil : un titre sans ligne se lit comme un bug (DT-22)
|
|
131
|
+
console.print(f" {msg.analyse_repartition_vide()}")
|
|
132
|
+
console.print()
|
|
133
|
+
n_echantillons = int(result.parametres.get("n_echantillons_effectif", 0))
|
|
134
|
+
console.print(msg.analyse_distorsion_titre(n_echantillons))
|
|
135
|
+
seuil = float(result.parametres.get("distorsion_max_ppm", 0))
|
|
136
|
+
for ligne in msg.analyse_bloc_distorsion(result.distorsions, seuil):
|
|
137
|
+
console.print(ligne)
|
|
138
|
+
console.print()
|
|
139
|
+
famille_libelle = msg.FAMILLE_LIBELLE.get(famille_cible, msg.FAMILLE_LIBELLE_DEFAUT)
|
|
140
|
+
ligne_reco = msg.analyse_recommandation(
|
|
141
|
+
reco.action, reco.cible_libelle, reco.cible_epsg, famille_libelle
|
|
142
|
+
)
|
|
143
|
+
_ligne(console, ligne_reco, tete="→ ", glyphe="[cyan]→[/cyan] ")
|
|
144
|
+
_ligne(console, msg.analyse_motif(reco.motif), tete=" ")
|
|
145
|
+
# DT-26 : la marque porte sur ce qui BOUGE. Préservation → signe positif ;
|
|
146
|
+
# changement de famille (repli CSRS, ≈ 1 m au Québec) → ⚠. Le glyphe et la
|
|
147
|
+
# couleur se décident ici : `messages.py` reste sans balisage Rich.
|
|
148
|
+
ligne_datum = msg.analyse_ligne_datum(result.famille, reco.cible_epsg, action=reco.action)
|
|
149
|
+
marque = msg.marque_datum(result.famille, famille_cible)
|
|
150
|
+
glyphe = "[green]✓[/green]" if marque == msg.MARQUE_DATUM_PRESERVE else "[yellow]⚠[/yellow]"
|
|
151
|
+
# tête de 4 caractères : « ␣␣ » + glyphe + « ␣ » ; seule sa LARGEUR compte,
|
|
152
|
+
# le glyphe balisé la remplace sur la première ligne.
|
|
153
|
+
_ligne(console, ligne_datum, tete=" ", glyphe=f" {glyphe} ")
|
|
154
|
+
note_datum = msg.analyse_note_datum(result.famille)
|
|
155
|
+
if note_datum is not None: # conseil neutre : ni ⚠, ni ✓
|
|
156
|
+
_ligne(console, note_datum, tete=" ")
|
|
157
|
+
for av in result.avertissements:
|
|
158
|
+
_ligne(console, av, tete=" ⚠ ", glyphe=" [yellow]⚠[/yellow] ")
|
|
159
|
+
alt_split = next((alt for alt in reco.alternatives if alt.get("action") == "split"), None)
|
|
160
|
+
if alt_split is not None:
|
|
161
|
+
console.print()
|
|
162
|
+
n_sorties = len(alt_split.get("zones", []))
|
|
163
|
+
_ligne(console, msg.analyse_ligne_alternative_split(n_sorties), tete=" ")
|
|
164
|
+
console.print()
|
|
165
|
+
if chemin_rapport is not None:
|
|
166
|
+
_ligne(
|
|
167
|
+
console,
|
|
168
|
+
msg.analyse_rapport(chemin_rapport.name),
|
|
169
|
+
tete="✓ ",
|
|
170
|
+
glyphe="[green]✓[/green] ",
|
|
171
|
+
)
|
|
172
|
+
# N2 (DT-29) : `Pour appliquer : crszone apply <couche>` s'affichait DANS
|
|
173
|
+
# `apply` lui-même : l'outil proposait la commande en cours. Elle ne
|
|
174
|
+
# s'adresse qu'au lecteur d'une ANALYSE, et seulement s'il y a quelque
|
|
175
|
+
# chose à appliquer (DT-22 : jamais quand `action == "aucune"`).
|
|
176
|
+
if suggerer_apply and reco.action != "aucune":
|
|
177
|
+
_ligne(console, msg.analyse_pour_appliquer(couche.name), tete=" ")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def mode_auto(console: Console, result: AnalysisResult) -> None:
|
|
181
|
+
"""Ligne « Mode --auto : … » entre le résumé abrégé et les sorties (CLI_UX §5)."""
|
|
182
|
+
reco = result.recommandation
|
|
183
|
+
console.print(msg.apply_mode_auto(reco.cible_libelle, reco.cible_epsg, action=reco.action))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def menu_options(console: Console, result: AnalysisResult) -> None:
|
|
187
|
+
"""Menu de décision interactif d'apply (CLI_UX §4)."""
|
|
188
|
+
for ligne in msg.apply_menu(result):
|
|
189
|
+
console.print(ligne)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def rappel_hors_seuil(console: Console, valeur_ppm: float) -> None:
|
|
193
|
+
"""Rappel affiché après le choix `[2]` si le fuseau dépasse le seuil (CLI_UX §4).
|
|
194
|
+
|
|
195
|
+
`valeur_ppm` : la valeur qui franchit réellement le seuil (DT-03), pas forcément
|
|
196
|
+
`max_ppm`, voir `messages.apply_hors_seuil`.
|
|
197
|
+
"""
|
|
198
|
+
console.print(f" [yellow]⚠[/yellow] {msg.apply_hors_seuil(valeur_ppm)}")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def succes_apply(console: Console, apply_result: ApplyResult) -> None:
|
|
202
|
+
"""Écran de succès d'apply (CLI_UX §4)."""
|
|
203
|
+
vert = "[green]✓[/green] "
|
|
204
|
+
for f in apply_result.fichiers:
|
|
205
|
+
_ligne(
|
|
206
|
+
console,
|
|
207
|
+
msg.apply_ligne_sortie(f.chemin, f.epsg, f.n_entites),
|
|
208
|
+
tete="✓ ",
|
|
209
|
+
glyphe=vert,
|
|
210
|
+
)
|
|
211
|
+
_ligne(
|
|
212
|
+
console,
|
|
213
|
+
msg.apply_ligne_journal(apply_result.journal),
|
|
214
|
+
tete="✓ ",
|
|
215
|
+
glyphe=vert,
|
|
216
|
+
)
|
|
217
|
+
for pipeline in apply_result.pipeline_proj:
|
|
218
|
+
_ligne(console, msg.apply_ligne_pipeline(pipeline))
|
|
219
|
+
for av in apply_result.avertissements:
|
|
220
|
+
_ligne(console, av, tete=" ⚠ ", glyphe=" [yellow]⚠[/yellow] ")
|