gpx2elev 0.3.7__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.
gpx2elev/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """gpx2elev desktop: the same spatial protocol as the Android application."""
2
+
3
+ from pathlib import Path
4
+
5
+ __version__ = (Path(__file__).parent / "assets/version.txt").read_text(encoding="utf-8").strip()
gpx2elev/__main__.py ADDED
@@ -0,0 +1,95 @@
1
+ import argparse
2
+ import json
3
+ import os
4
+ from pathlib import Path
5
+ import sys
6
+
7
+ from . import __version__
8
+
9
+
10
+ def main(argv=None):
11
+ parser = argparse.ArgumentParser(description="gpx2elev : estimation du dénivelé d'un GPX")
12
+ parser.add_argument("gpx", nargs="?", help="GPX à ouvrir")
13
+ parser.add_argument("--version", action="version", version=__version__)
14
+ parser.add_argument("--calculate", action="store_true", help="Calcul sans interface graphique")
15
+ parser.add_argument("--offline", action="store_true", help="Utiliser uniquement les profils en cache")
16
+ parser.add_argument("--model", choices=["IGN", "MAPTERHORN", "FABDEM", "COPERNICUS", "SRTM"], help="Forcer un modèle au lieu du repli automatique")
17
+ parser.add_argument("--data-dir", type=Path, help="Dossier de données et de cache")
18
+ parser.add_argument("--output", type=Path, help="Résultat JSON du calcul sans interface")
19
+ parser.add_argument("--csv", type=Path, help="Résultats CSV du calcul sans interface")
20
+ parser.add_argument("--self-test-report", type=Path, help=argparse.SUPPRESS)
21
+ parser.add_argument("--apply-update", type=Path, help=argparse.SUPPRESS)
22
+ parser.add_argument("--update-result", type=Path, help=argparse.SUPPRESS)
23
+ args = parser.parse_args(argv)
24
+ if args.apply_update:
25
+ from .updates import apply_install
26
+ return apply_install(args.apply_update)
27
+ if args.self_test_report:
28
+ # No display server is required for the automated packaged GUI check.
29
+ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
30
+ try:
31
+ from .smoke import run_smoke
32
+ return run_smoke(args.self_test_report)
33
+ except Exception:
34
+ import traceback
35
+ args.self_test_report.parent.mkdir(parents=True, exist_ok=True)
36
+ args.self_test_report.write_text(json.dumps({"status": "ERROR", "traceback": traceback.format_exc()}, ensure_ascii=False, indent=2), encoding="utf-8")
37
+ return 1
38
+ if args.calculate:
39
+ if not args.gpx:
40
+ parser.error("--calculate demande un fichier GPX")
41
+ from PySide6.QtCore import QCoreApplication
42
+ from .providers import Source
43
+ from .service import calculate, export_json, export_summary
44
+ from .ui import data_directory
45
+ core_app = QCoreApplication.instance() or QCoreApplication(["gpx2elev"])
46
+ core_app.setApplicationName("gpx2elev")
47
+ core_app.setOrganizationName("Nico")
48
+ try:
49
+ result = calculate(args.gpx, args.data_dir or data_directory(), not args.offline, Source[args.model] if args.model else None)
50
+ if args.output:
51
+ export_json(result, args.output)
52
+ if args.csv:
53
+ export_summary(result, args.csv)
54
+ if sys.stdout is not None:
55
+ print(json.dumps(result.summary(), ensure_ascii=False, indent=2))
56
+ return 0
57
+ except Exception as exc:
58
+ if args.output:
59
+ args.output.parent.mkdir(parents=True, exist_ok=True)
60
+ args.output.write_text(json.dumps({"erreur": str(exc)}, ensure_ascii=False), encoding="utf-8")
61
+ if sys.stderr is not None:
62
+ print(str(exc), file=sys.stderr)
63
+ return 2
64
+ from .providers import Source
65
+ from .ui import DesktopApplication, MainWindow
66
+ from PySide6.QtCore import QTimer
67
+ app = DesktopApplication(sys.argv if argv is None else ["gpx2elev"] + argv)
68
+ window = MainWindow(args.data_dir, restore=not (args.gpx or app.pending_path))
69
+ app.window = window
70
+ if args.offline:
71
+ window.online.setChecked(False)
72
+ if args.model:
73
+ window.models.setCurrentIndex(window.models.findData(Source[args.model]))
74
+ path = args.gpx or app.pending_path
75
+ if path:
76
+ QTimer.singleShot(0, lambda: window.open_path(path))
77
+ window.show()
78
+ if args.update_result:
79
+ try:
80
+ installed = json.loads(args.update_result.read_text(encoding="utf-8"))
81
+ if installed.get("status") == "ERROR":
82
+ from PySide6.QtWidgets import QMessageBox
83
+ QTimer.singleShot(0, lambda: QMessageBox.warning(window, window.tr("Mises à jour"),
84
+ window.tr("La mise à jour a échoué. L'ancienne version a été restaurée.")))
85
+ except (OSError, ValueError):
86
+ pass
87
+ code = app.exec()
88
+ if window.pending_update is not None:
89
+ # The helper also waits for this process to terminate, releasing all bundled DLLs.
90
+ (window.pending_update.parent / "ready").write_text("ready", encoding="utf-8")
91
+ return code
92
+
93
+
94
+ if __name__ == "__main__":
95
+ raise SystemExit(main())
Binary file
@@ -0,0 +1,93 @@
1
+ Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ https://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,259 @@
1
+ {
2
+ "Le relief de votre parcours": "Your route's terrain",
3
+ "Le dénivelé de votre parcours": "Your route's elevation gain",
4
+ "Le dénivelé de votre parcours, à partir du terrain": "Your route's elevation gain, from terrain data",
5
+ "Annuler le calcul": "Cancel calculation",
6
+ "Importer une autre trace GPX": "Import another GPX track",
7
+ "Importer une trace GPX": "Import a GPX track",
8
+ "Fermer le message": "Dismiss message",
9
+ "Le premier calcul utilise Internet. Les données téléchargées restent disponibles pour cette trace hors connexion.": "The first calculation uses the Internet. Downloaded data remain available offline for this track.",
10
+ "Importez votre parcours pour calculer sa montée, sa descente et son profil d'altitude à partir des modèles de terrain.": "Import your route to calculate its ascent, descent and elevation profile from terrain models.",
11
+ "Votre parcours": "Your route",
12
+ "Calcul en cours": "Calculating",
13
+ "{0} / {1} positions": "{0} / {1} positions",
14
+ "Lecture des altitudes, puis lissage du profil et cumul des montées.": "Reading elevations, then smoothing the profile and accumulating ascent.",
15
+ "Calcul indisponible": "Calculation unavailable",
16
+ "Réessayer": "Retry",
17
+ "DÉNIVELÉ POSITIF": "ELEVATION GAIN",
18
+ "DÉNIVELÉ POSITIF ESTIMÉ": "ESTIMATED ELEVATION GAIN",
19
+ "DÉNIVELÉ NÉGATIF ESTIMÉ": "ESTIMATED ELEVATION LOSS",
20
+ "Altitudes conservées sur cet appareil": "Elevations cached on this device",
21
+ "Profil complet · {0} positions": "Complete profile · {0} positions",
22
+ "DESCENTE · D−": "DESCENT · D−",
23
+ "DISTANCE DU PARCOURS": "ROUTE DISTANCE",
24
+ "Montée": "Ascent",
25
+ "Descente": "Descent",
26
+ "Sur les coordonnées GPX": "From GPX coordinates",
27
+ "Modèle de repli utilisé": "Fallback model used",
28
+ "{0} a fourni le premier profil complet.": "{0} provided the first complete profile.",
29
+ "Masquer les valeurs du GPX": "Hide GPX values",
30
+ "Comparer avec les altitudes du GPX": "Compare with GPX elevations",
31
+ "Somme brute des variations": "Raw sum of elevation changes",
32
+ "Altitudes GPX manquantes.": "Missing GPX elevations.",
33
+ "GPX filtré · même protocole": "Filtered GPX · same protocol",
34
+ "{0} points d'origine · {1} segment(s)": "{0} original points · {1} segment(s)",
35
+ "Exporter le résultat en CSV": "Export result as CSV",
36
+ "Profil d'altitude": "Elevation profile",
37
+ "Profil lissé (terrain)": "Smoothed profile (terrain)",
38
+ "Mesures GPX": "GPX measurements",
39
+ "Profils d'altitude : terrain lissé et mesures GPX, distance en kilomètres et altitude en mètres": "Elevation profiles: smoothed terrain and GPX measurements, distance in kilometres and elevation in metres",
40
+ "Zoom +": "Zoom +",
41
+ "Zoom −": "Zoom −",
42
+ "Zoom avant": "Zoom in",
43
+ "Zoom arrière": "Zoom out",
44
+ "Vue complète": "Full view",
45
+ "Plein écran": "Full screen",
46
+ "Quitter le plein écran": "Exit full screen",
47
+ "Le profil n'a pas pu être enregistré dans le cache. Le résultat reste disponible, mais son utilisation hors connexion n'est pas assurée.": "The profile could not be saved to the cache. The result is available, but offline use cannot be guaranteed.",
48
+ "Pincez pour zoomer, glissez pour déplacer la vue.": "Pinch to zoom, drag to pan.",
49
+ "Altitudes totales": "Overall elevations",
50
+ "Altitude visible : {0} à {1} m": "Visible elevation: {0} to {1} m",
51
+ "Molette : zoom · glisser : déplacer · double-clic : vue complète": "Mouse wheel: zoom · drag: pan · double-click: full view",
52
+ "Protocole de calcul": "Calculation protocol",
53
+ "Pas 5 m": "Step 5 m",
54
+ "Seuil 2 m": "Threshold 2 m",
55
+ "Réduire les explications": "Hide explanation",
56
+ "Méthode et sources": "Method and sources",
57
+ "Le parcours est rééchantillonné tous les 5 m. Seules les altitudes sont lissées, avec une gaussienne de σ = 20 m. Une montée ou descente est confirmée après un retournement de 2 m ; les petites hausses successives restent cumulées.": "The route is resampled every 5 m. Only elevations are smoothed, using a Gaussian with σ = 20 m. A reversal of 2 m confirms an ascent or descent; successive small rises still accumulate.",
58
+ "Ordre de repli : ": "Fallback order: ",
59
+ ". Le premier modèle couvrant toute la trace fournit le profil. Cet ordre vient de la comparaison en France métropolitaine.": ". The first model covering the entire track provides the profile. This order comes from the comparison in mainland France.",
60
+ "Les coordonnées GPX sont conservées. Près d'une falaise, leur précision peut fortement influencer le résultat. Le D+ affiché est une estimation selon ce protocole.": "GPX coordinates are preserved. Near a cliff, their accuracy can strongly affect the result. The displayed elevation gain is an estimate using this protocol.",
61
+ "Les coordonnées sont envoyées au service IGN pour lire les altitudes. Les autres modèles utilisent leurs tuiles publiques. GPX et altitudes sont conservés sur cet appareil.": "Coordinates are sent to IGN to read elevations. Other models use their public tiles. GPX tracks and elevations are stored on this device.",
62
+ "Données : IGN (Licence Ouverte) ; Mapterhorn et ses producteurs (mapterhorn.com/attribution) ; FABDEM 1.2, University of Bristol (CC BY-NC-SA 4.0) ; Copernicus DEM © DLR e.V., Airbus Defence and Space GmbH, financé par l'Union européenne ; SRTM NASA/USGS, miroir Kurviger.": "Data: IGN (Open Licence); Mapterhorn and its providers (mapterhorn.com/attribution); FABDEM 1.2, University of Bristol (CC BY-NC-SA 4.0); Copernicus DEM © DLR e.V., Airbus Defence and Space GmbH, funded by the European Union; SRTM NASA/USGS, Kurviger mirror.",
63
+ "Choisir un GPX": "Choose a GPX",
64
+ "Recalculer": "Recalculate",
65
+ "Modèle automatique": "Automatic model",
66
+ "Modèle d'altitude": "Elevation model",
67
+ "Internet autorisé": "Allow Internet access",
68
+ "Déposez un fichier GPX dans cette fenêtre, ou choisissez-le ci-dessus.": "Drop a GPX file into this window, or choose one above.",
69
+ "Source : sélection automatique selon la couverture": "Source: automatic selection based on coverage",
70
+ "Le profil d’altitude apparaîtra ici": "The elevation profile will appear here",
71
+ "Profil d'altitude lissé, distance en kilomètres et altitude en mètres": "Smoothed elevation profile, distance in kilometres and elevation in metres",
72
+ "Profil lissé · pas 5 m · gaussienne σ = 20 m · hystérésis 2 m": "Smoothed profile · step 5 m · Gaussian σ = 20 m · hysteresis 2 m",
73
+ "Altitudes GPX : les sommes brutes et filtrées seront affichées après le calcul.": "GPX elevations: raw and filtered totals will appear after the calculation.",
74
+ "Exporter les résultats CSV": "Export results as CSV",
75
+ "Exporter le profil CSV": "Export profile as CSV",
76
+ "Annuler": "Cancel",
77
+ "Prêt": "Ready",
78
+ "Choisir une trace": "Choose a track",
79
+ "Traces GPX (*.gpx *.GPX);;Tous les fichiers (*)": "GPX tracks (*.gpx *.GPX);;All files (*)",
80
+ "Calcul en cours…": "Calculating…",
81
+ "Recherche du modèle d'altitude…": "Finding an elevation model…",
82
+ "Calcul annulé.": "Calculation cancelled.",
83
+ " · profil en cache": " · cached profile",
84
+ "Source : ": "Source: ",
85
+ "Altitude {0}–{1} m · pas 5 m · σ = 20 m · hystérésis 2 m": "Elevation {0}–{1} m · step 5 m · σ = 20 m · hysteresis 2 m",
86
+ "Terrain lissé {0}–{1} m · pas 5 m · σ = 20 m · hystérésis 2 m": "Smoothed terrain {0}–{1} m · step 5 m · σ = 20 m · hysteresis 2 m",
87
+ "altitudes absentes ou incomplètes": "missing or incomplete elevations",
88
+ "Comparaison GPX\nSomme brute des variations : ": "GPX comparison\nRaw sum of elevation changes: ",
89
+ "\nGPX rééchantillonné et filtré : ": "\nResampled and filtered GPX: ",
90
+ "Repli entre modèles :\n": "Model fallback:\n",
91
+ "Calcul terminé · {0} points GPX · {1} positions à 5 m": "Calculation complete · {0} GPX points · {1} positions at 5 m",
92
+ "Calcul impossible. Consultez le détail ci-dessus.": "Calculation failed. See the details above.",
93
+ "Aucun profil complet disponible": "No complete profile available",
94
+ "Les altitudes n'ont pas pu être calculées.": "Elevations could not be calculated.",
95
+ "Annulation en cours…": "Cancelling…",
96
+ "Exporter en CSV": "Export as CSV",
97
+ "Fichiers CSV (*.csv)": "CSV files (*.csv)",
98
+ "Export enregistré : ": "Export saved: ",
99
+ "Export impossible": "Export failed",
100
+ "Lecture de la trace GPX": "Reading the GPX track",
101
+ "Lecture du GPX": "Reading GPX",
102
+ "Lissage et calcul du dénivelé": "Smoothing and calculating elevation gain",
103
+ "Lissage du profil et calcul du dénivelé": "Smoothing the profile and calculating elevation gain",
104
+ "Recherche des altitudes · ": "Finding elevations · ",
105
+ "Le profil à conserver est incomplet.": "The profile to cache is incomplete.",
106
+ "Profil absent du cache hors connexion.": "Profile not cached for offline use.",
107
+ "Le modèle ne couvre pas toutes les positions de la trace.": "The model does not cover every position in the track.",
108
+ "Cette trace n'est pas encore disponible hors connexion. Activez Internet pour son premier calcul.": "This track is not yet available offline. Enable Internet access for its first calculation.",
109
+ "Aucun modèle n'a fourni un profil complet.": "No model provided a complete profile.",
110
+ "Le GPX dépasse la limite de 20 Mo.": "The GPX exceeds the 20 MB limit.",
111
+ "Le GPX dépasse 20 Mo.": "The GPX exceeds 20 MB.",
112
+ "Fichier XML invalide ou contenant une déclaration DTD interdite.": "Invalid XML file or prohibited DTD declaration.",
113
+ "Ce fichier n'est pas un GPX 1.0 ou 1.1.": "This file is not a GPX 1.0 or 1.1 file.",
114
+ "Le GPX dépasse {0} points.": "The GPX exceeds {0} points.",
115
+ "Coordonnées invalides au point {0}.": "Invalid coordinates at point {0}.",
116
+ "Le GPX ne contient pas de trace ou de route utilisable.": "The GPX contains no usable track or route.",
117
+ "Trace trop longue : maximum {0} positions à 5 m.": "Track too long: maximum {0} positions at 5 m.",
118
+ "Le GPX ne contient aucun parcours avec deux positions distinctes.": "The GPX contains no route with two distinct positions.",
119
+ "Profil ou paramètres gaussiens invalides.": "Invalid profile or Gaussian parameters.",
120
+ "Profil ou seuil d'hystérésis invalide.": "Invalid profile or hysteresis threshold.",
121
+ "Altitudes non finies.": "Non-finite elevations.",
122
+ "Incohérence entre montée, descente et extrémités.": "Inconsistency between ascent, descent and endpoints.",
123
+ "Le profil d'altitude est incomplet.": "The elevation profile is incomplete.",
124
+ "Impossible d'ouvrir ce fichier GPX.": "Unable to open this GPX file.",
125
+ "Le calcul a échoué.": "Calculation failed.",
126
+ "Impossible de créer le fichier CSV.": "Unable to create the CSV file.",
127
+ "Résultat exporté.": "Result exported.",
128
+ "D+ estimé : rééchantillonnage à 5 m, gaussienne spatiale σ = 20 m (rayon 80 m), hystérésis de 2 m. Les segments et leurs extrémités sont conservés. Les coordonnées XY ne sont pas corrigées.": "Estimated elevation gain: resampling at 5 m, spatial Gaussian σ = 20 m (radius 80 m), hysteresis of 2 m. Segments and their endpoints are preserved. XY coordinates are not corrected.",
129
+ "Ordre automatique : IGN LiDAR HD → Mapterhorn → FABDEM → Copernicus GLO-30 → SRTM90. Un seul modèle couvre toute la trace. Le GPX brut reste une comparaison distincte.": "Automatic order: IGN LiDAR HD → Mapterhorn → FABDEM → Copernicus GLO-30 → SRTM90. One model covers the entire track. Raw GPX remains a separate comparison.",
130
+ "Le premier calcul demande Internet. Les coordonnées sont envoyées à l'IGN ; les autres modèles sont lus dans des tuiles publiques. Les profils et la dernière trace sont conservés localement. Aucune télémétrie.": "The first calculation needs Internet access. Coordinates are sent to IGN; other models are read from public tiles. Profiles and the last track are stored locally. No telemetry.",
131
+ "Mapterhorn : producteurs": "Mapterhorn: providers",
132
+ "Copernicus : licence DEM": "Copernicus: DEM licence",
133
+ "miroir Kurviger": "Kurviger mirror",
134
+ "Interface Qt/PySide6 sous LGPLv3. Les licences des dépendances sont incluses dans le bundle.": "Qt/PySide6 interface under LGPLv3. Dependency licences are included in the bundle.",
135
+ "Réponse HTTP partielle incohérente.": "Inconsistent partial HTTP response.",
136
+ "Position négative dans l'archive.": "Negative position in the archive.",
137
+ "Réponse IGN invalide.": "Invalid IGN response.",
138
+ "Téléchargement trop volumineux.": "Download is too large.",
139
+ "Calcul annulé": "Calculation cancelled",
140
+ "Le serveur ne permet pas la lecture partielle des données.": "The server does not support partial data reads.",
141
+ "Échec réseau.": "Network failure.",
142
+ "Réponse du serveur trop volumineuse.": "Server response is too large.",
143
+ "Réponse partielle sans position.": "Partial response without a position.",
144
+ "Position de réponse incorrecte.": "Incorrect response position.",
145
+ "Fichier raster tronqué.": "Truncated raster file.",
146
+ "Lecture partielle incomplète.": "Incomplete partial read.",
147
+ "Répertoire ZIP invalide.": "Invalid ZIP directory.",
148
+ "Archive ZIP chiffrée.": "Encrypted ZIP archive.",
149
+ "Données ZIP incomplètes ou corrompues.": "Incomplete or corrupt ZIP data.",
150
+ "En-tête GeoTIFF invalide.": "Invalid GeoTIFF header.",
151
+ "Raster tuilé requis.": "A tiled raster is required.",
152
+ "Impossible de supprimer la tuile corrompue.": "Unable to remove the corrupt tile.",
153
+ "Raster attendu à une bande.": "A single-band raster is required.",
154
+ "Projection raster différente de WGS84.": "Raster projection is not WGS84.",
155
+ "Balise TIFF non entière.": "Non-integer TIFF tag.",
156
+ "Bloc raster tronqué.": "Truncated raster block.",
157
+ "Ce fichier n'est pas un GPX.": "This file is not a GPX file.",
158
+ "Les déclarations DTD ne sont pas acceptées dans un GPX.": "DTD declarations are not accepted in GPX files.",
159
+ "Référence XML externe refusée.": "External XML reference rejected.",
160
+ "Altitudes absentes du cache hors connexion.": "Elevations are not cached for offline use.",
161
+ "Nombre d'altitudes incorrect.": "Incorrect number of elevations.",
162
+ "{0} positions sans altitude utilisable sur {1}.": "{0} positions have no usable elevation out of {1}.",
163
+ "Lecture des altitudes impossible.": "Unable to read elevations.",
164
+ "Aucun modèle n'a pu fournir un profil complet.": "No model could provide a complete profile.",
165
+ "Tuile Mapterhorn illisible.": "Unreadable Mapterhorn tile.",
166
+ "Réponse IGN incomplète.": "Incomplete IGN response.",
167
+ "Altitudes ": "Elevations ",
168
+ "La trace sort de la couverture IGN LiDAR HD.": "The track is outside IGN LiDAR HD coverage.",
169
+ "Position hors de la couverture Mercator.": "Position outside Mercator coverage.",
170
+ "Tuile Mapterhorn absente.": "Missing Mapterhorn tile.",
171
+ "Latitude hors de la couverture Mapterhorn.": "Latitude outside Mapterhorn coverage.",
172
+ "Dimensions de tuiles incohérentes.": "Inconsistent tile dimensions.",
173
+ "Taille de l'archive FABDEM inconnue.": "Unknown FABDEM archive size.",
174
+ "Tuile FABDEM {0} absente.": "Missing FABDEM tile {0}.",
175
+ "Téléchargement de la tuile FABDEM {0}": "Downloading FABDEM tile {0}",
176
+ "Archive SRTM vide.": "Empty SRTM archive.",
177
+ "Tuile SRTM tronquée.": "Truncated SRTM tile.",
178
+ "Tuile SRTM {0} absente.": "Missing SRTM tile {0}.",
179
+ "Latitude hors de la couverture SRTM90.": "Latitude outside SRTM90 coverage.",
180
+ "Cache des altitudes": "Elevation cache",
181
+ "Profils": "Profiles",
182
+ "Tuiles": "Tiles",
183
+ "Mio": "MiB",
184
+ "Kio": "KiB",
185
+ "octets": "bytes",
186
+ "Vider le cache": "Clear cache",
187
+ "Vider le cache ?": "Clear cache?",
188
+ "Les altitudes devront être téléchargées à nouveau. Le GPX, les réglages et le résultat affiché sont conservés.": "Elevations will need to be downloaded again. The GPX, settings and displayed result are kept.",
189
+ "Suppression du cache…": "Clearing cache…",
190
+ "Cache vidé.": "Cache cleared.",
191
+ "Impossible de vider complètement le cache.": "Unable to completely clear the cache.",
192
+ "Taille du cache indisponible.": "Cache size unavailable.",
193
+ "Menu": "Menu",
194
+ "Mises à jour": "Updates",
195
+ "Version installée : {0}": "Installed version: {0}",
196
+ "Version {0} disponible · {1} Mio": "Version {0} available · {1} MiB",
197
+ "Recherche de mises à jour…": "Checking for updates…",
198
+ "Vous utilisez la dernière version publiée.": "You are using the latest published version.",
199
+ "Une nouvelle version est disponible.": "A new version is available.",
200
+ "Téléchargement de la mise à jour…": "Downloading update…",
201
+ "Préparation de l'installation…": "Preparing installation…",
202
+ "Vérification de la mise à jour…": "Verifying update…",
203
+ "Télécharger et installer": "Download and install",
204
+ "Télécharger": "Download",
205
+ "Installer et redémarrer": "Install and restart",
206
+ "Autoriser l'installation": "Allow installation",
207
+ "Installer": "Install",
208
+ "Vérifier à nouveau": "Check again",
209
+ "Fermer": "Close",
210
+ "Mise à jour vérifiée. L'application va se fermer puis redémarrer.": "Update verified. The application will close and restart.",
211
+ "Mise à jour vérifiée. Vous pouvez lancer l'installation.": "Update verified. You can start installation.",
212
+ "Mise à jour téléchargée. Ouvrez le dossier pour lancer la nouvelle version.": "Update downloaded. Open the folder to launch the new version.",
213
+ "Ouvrir le dossier": "Open folder",
214
+ "Android demande votre autorisation pour installer une mise à jour depuis gpx2elev. Activez cette autorisation dans les réglages, puis revenez ici.": "Android needs your permission to install an update from gpx2elev. Enable this permission in settings, then return here.",
215
+ "Confirmez l'installation dans la fenêtre Android. Si vous l'avez annulée, vous pouvez réessayer.": "Confirm installation in the Android window. If you cancelled it, you can try again.",
216
+ "Attendez la fin du calcul avant d'installer la mise à jour.": "Wait for the calculation to finish before installing the update.",
217
+ "Aucune mise à jour disponible pour ce système.": "No update is available for this system.",
218
+ "Version de mise à jour invalide.": "Invalid update version.",
219
+ "Réponse de mise à jour invalide.": "Invalid update response.",
220
+ "Le fichier de mise à jour de ce système est absent.": "The update file for this system is missing.",
221
+ "Empreinte de mise à jour invalide.": "Invalid update checksum.",
222
+ "Empreinte de mise à jour absente.": "Missing update checksum.",
223
+ "Adresse de mise à jour invalide.": "Invalid update URL.",
224
+ "GitHub limite les vérifications. Réessayez plus tard.": "GitHub is limiting update checks. Try again later.",
225
+ "Aucune release publiée pour le moment.": "No release has been published yet.",
226
+ "Le serveur de mises à jour est indisponible.": "The update server is unavailable.",
227
+ "Réponse de mise à jour trop volumineuse.": "Update response is too large.",
228
+ "Fichier de mise à jour incomplet ou corrompu.": "Incomplete or corrupt update file.",
229
+ "Archive de mise à jour invalide.": "Invalid update archive.",
230
+ "Archive de mise à jour trop volumineuse.": "Update archive is too large.",
231
+ "Le programme de mise à jour est absent de l'archive.": "The update executable is missing from the archive.",
232
+ "Le dossier de données doit être situé hors du dossier de l'application pour la mise à jour.": "The data folder must be outside the application folder to install an update.",
233
+ "Plan de mise à jour invalide.": "Invalid update installation plan.",
234
+ "Connexion au serveur de mises à jour impossible. Vérifiez Internet et réessayez.": "Unable to connect to the update server. Check your Internet connection and try again.",
235
+ "Impossible d'écrire la mise à jour. Vérifiez l'espace libre et les droits du dossier.": "Unable to save the update. Check free disk space and folder permissions.",
236
+ "Impossible de lancer l'installation de la mise à jour.": "Unable to start update installation.",
237
+ "Impossible de télécharger la mise à jour. Vérifiez Internet, l'espace libre et réessayez.": "Unable to download the update. Check your Internet connection and free space, then try again.",
238
+ "La vérification de la mise à jour a échoué.": "Update verification failed.",
239
+ "L'APK ne correspond pas à cette application ou à une version plus récente.": "The APK is not for this application or a newer version.",
240
+ "La signature de la mise à jour ne correspond pas à l'application installée.": "The update signature does not match the installed application.",
241
+ "L'APK de mise à jour est illisible.": "The update APK cannot be read.",
242
+ "L'autorisation d'installer la mise à jour n'a pas été accordée.": "Permission to install the update was not granted.",
243
+ "Impossible d'ouvrir l'installateur Android.": "Unable to open the Android installer.",
244
+ "Impossible d'ouvrir les réglages d'installation Android.": "Unable to open Android installation settings.",
245
+ "Le programme téléchargé n'a pas réussi sa vérification.": "The downloaded application failed verification.",
246
+ "La mise à jour a échoué. L'ancienne version a été restaurée.": "The update failed. The previous version has been restored.",
247
+ "Une installation déjà lancée ne peut pas être annulée.": "An installation that has already started cannot be cancelled.",
248
+ "Distance · Heure locale": "Distance · Local time",
249
+ "Distance · Heures GPX absentes.": "Distance · GPX timestamps missing.",
250
+ "Distance : {0} km": "Distance: {0} km",
251
+ "Heure locale : ": "Local time: ",
252
+ "Terrain lissé : {0} m": "Smoothed terrain: {0} m",
253
+ "Mesure GPX : {0} m": "GPX measurement: {0} m",
254
+ "GPX interpolé : {0} m": "Interpolated GPX: {0} m",
255
+ "Touchez la courbe pour lire les valeurs.": "Tap the chart to read its values.",
256
+ "Lire les valeurs de la courbe": "Read chart values",
257
+ "Pincez pour zoomer, glissez pour déplacer, touchez pour lire les valeurs.": "Pinch to zoom, drag to pan, tap to read values.",
258
+ "Molette : zoom · glisser : déplacer · clic : lire les valeurs · double-clic : vue complète": "Mouse wheel: zoom · drag: pan · click: read values · double-click: full view"
259
+ }
@@ -0,0 +1 @@
1
+ 0.3.7
gpx2elev/cache.py ADDED
@@ -0,0 +1,40 @@
1
+ """Manage downloaded data only; never touch the GPX, settings or exports."""
2
+ import os
3
+ from pathlib import Path
4
+
5
+
6
+ def cache_files(directory):
7
+ def fail(error):
8
+ raise error
9
+
10
+ for name in ("profiles", "tiles"):
11
+ root = Path(directory) / name
12
+ if root.is_symlink():
13
+ yield root
14
+ continue
15
+ if not root.exists():
16
+ continue
17
+ for folder, dirs, files in os.walk(root, followlinks=False, onerror=fail):
18
+ for child in dirs[:]:
19
+ path = Path(folder) / child
20
+ if path.is_symlink():
21
+ dirs.remove(child)
22
+ yield path
23
+ for child in files:
24
+ yield Path(folder) / child
25
+
26
+
27
+ def cache_sizes(directory):
28
+ sizes = {"profiles": 0, "tiles": 0}
29
+ base = Path(directory)
30
+ for path in cache_files(base):
31
+ if not path.is_symlink():
32
+ sizes[path.relative_to(base).parts[0]] += path.stat().st_size
33
+ return sizes
34
+
35
+
36
+ def clear_cache(directory):
37
+ # Unlink files and links, including abandoned .part files; do not follow links.
38
+ # Empty directories are harmless and reusable by the readers.
39
+ for path in cache_files(directory):
40
+ path.unlink(missing_ok=True)
gpx2elev/chart_data.py ADDED
@@ -0,0 +1,98 @@
1
+ """Native GPX observations, time ticks and a cursor independent of draw decimation."""
2
+ from dataclasses import dataclass
3
+ from datetime import datetime, timezone
4
+ import math
5
+
6
+ import numpy as np
7
+
8
+ from .core import distances
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class Selection:
13
+ distance: float
14
+ time: float | None
15
+ terrain: float | None
16
+ gpx: float | None
17
+ interpolated: bool = False
18
+
19
+
20
+ class ChartObservations:
21
+ def __init__(self, prepared, terrain):
22
+ xs, zs, times, groups = [], [], [], []
23
+ self.terrain = []
24
+ offset = 0.0
25
+ terrain_index = 0
26
+ for group, segment in enumerate(prepared.track.segments):
27
+ x = distances(segment.coordinates)
28
+ xs.append(x + offset)
29
+ zs.append(segment.elevations)
30
+ times.append(segment.times if segment.times is not None else np.full(len(x), np.nan))
31
+ groups.append(np.full(len(x), group, dtype=int))
32
+ self.terrain.append(terrain[terrain_index] if len(x) >= 2 and x[-1] > 0 else None)
33
+ if len(x) >= 2 and x[-1] > 0:
34
+ terrain_index += 1
35
+ offset += x[-1]
36
+ self.distance = np.concatenate(xs)
37
+ self.elevation = np.concatenate(zs)
38
+ self.times = np.concatenate(times)
39
+ self.groups = np.concatenate(groups)
40
+ self.has_time = bool(np.isfinite(self.times).any())
41
+
42
+ def time_at(self, distance):
43
+ """Interpolate adjacent times only, without crossing gaps, segments or clock reversals."""
44
+ end = int(np.searchsorted(self.distance, distance, side="right"))
45
+ if end and self.distance[end - 1] == distance:
46
+ value = self.times[end - 1]
47
+ return float(value) if np.isfinite(value) else None
48
+ if end == 0 or end == len(self.distance):
49
+ return None
50
+ a, b = end - 1, end
51
+ start, finish = self.times[a], self.times[b]
52
+ if self.groups[a] != self.groups[b] or not np.isfinite([start, finish]).all() or finish < start:
53
+ return None
54
+ fraction = (distance - self.distance[a]) / (self.distance[b] - self.distance[a])
55
+ return float(start + fraction * (finish - start))
56
+
57
+ def select(self, distance, elevation, visible=None):
58
+ at = int(np.searchsorted(self.distance, distance))
59
+ neighbours = {max(0, min(len(self.distance) - 1, at)), max(0, min(len(self.distance) - 1, at - 1))}
60
+ closest = min(abs(self.distance[i] - distance) for i in neighbours)
61
+ candidates = []
62
+ for i in sorted(neighbours):
63
+ if abs(self.distance[i] - distance) > closest + 1e-9:
64
+ continue
65
+ start = int(np.searchsorted(self.distance, self.distance[i], side="left"))
66
+ end = int(np.searchsorted(self.distance, self.distance[i], side="right"))
67
+ candidates.extend(range(start, end))
68
+ # Multiple measurements at a stop share x; select the closest recorded elevation to the tap.
69
+ index = min(set(candidates), key=lambda i: (abs(self.elevation[i] - elevation)
70
+ if np.isfinite(self.elevation[i]) else math.inf, -i))
71
+ x = float(self.distance[index])
72
+ if visible is not None and not visible[0] <= x <= visible[1]:
73
+ # A zoomed view may contain no native observation. Read the visible line at the tap.
74
+ end = int(np.searchsorted(self.distance, distance, side="right"))
75
+ if 0 < end < len(self.distance) and self.groups[end - 1] == self.groups[end]:
76
+ a, b = end - 1, end
77
+ fraction = (distance - self.distance[a]) / (self.distance[b] - self.distance[a])
78
+ gpx = ((1 - fraction) * self.elevation[a] + fraction * self.elevation[b]
79
+ if np.isfinite(self.elevation[a:b + 1]).all() else None)
80
+ profile = self.terrain[self.groups[a]]
81
+ terrain = float(np.interp(distance, profile.distance, profile.elevations)) if profile is not None else None
82
+ return Selection(float(distance), self.time_at(distance), terrain, None if gpx is None else float(gpx), True)
83
+ profile = self.terrain[self.groups[index]]
84
+ terrain = float(np.interp(x, profile.distance, profile.elevations)) if profile is not None else None
85
+ time = float(self.times[index]) if np.isfinite(self.times[index]) else None
86
+ gpx = float(self.elevation[index]) if np.isfinite(self.elevation[index]) else None
87
+ return Selection(x, time, terrain, gpx)
88
+
89
+
90
+ def local_time(value, seconds=False, language="fr", date=False, zone=None):
91
+ if value is None:
92
+ return "—"
93
+ try:
94
+ time = datetime.fromtimestamp(value, timezone.utc).astimezone(zone)
95
+ clock = time.strftime("%H:%M:%S" if seconds else "%H:%M")
96
+ return time.strftime("%d/%m/%Y " if language == "fr" else "%Y-%m-%d ") + clock if date else clock
97
+ except (ValueError, OverflowError, OSError):
98
+ return "—"