Si j'ai été absent pendant plusieurs semaines quant à la publication d'articles sur mon site personnel, c'est simplement car je mettais tout en œuvre de mon côté pour travailler sur un projet personnel qui devrait à terme déboucher sur un projet plus associatif dans les mois à venir, le développement d'un petit outil PHP qui permet de parser un fichier GPX, vous savez les fichiers qui enregistrent des parcours de marche, afin de pouvoir, une fois mon fichier GPX récupérer sur ma machine, afficher directement d'une part la carte avec le tracé du contenu du fichier, mais aussi toute une panoplie d'informations qui sont très souvent stockées dans ce même fichier. Pour celles et ceux que ca interesse et qui veulent aller plus loin,, je ,ne peux que vous conseiller la lecture (en anglais) de la page de description du format à l'adresse https://www.topografix.com/gpx/1/1/
Quoi récupérer ?
À partir de ce fichier, je récupère plusieurs informations. Notons que dans le projet que je vous propose ici, je qualifie le fichier GPX de complet. (complet dans le sens où j'ai toutes les infos qui m'interesse).
En effet, un fichier au format GPX peut contenir des informations facultatives, comme par exemple les instructions de navigation, des méta-données et peut être même d'autres informations que je n'ai pas vu. Les premières informations très importantes de ce fichier sont en effet les informations contenant l'ensemble des données GPS du tracé. On appelle en effet tracé, la trace, c'est-à-dire l'ensemble des points sur lesquels un usager est généralement passé. Effectivement, ce terme peut-être galvaudé dans la mesure où le dessin effectué peut ne pas avoir été généré par une trace, mais plus par un outil logiciel.
La classe
Pour mener, à bien mon projet, j'ai utilisé une classe gpx4 (oui, il m'a fallu 4 essais pour arriver à mon but) , que j'ai écrite.
Non allez je vais aller un peu plus loin, vous savez très bien que j'ai une sainte horreur de la programmation objet, j'ai utilisé pour une fois une (en fait deux) intelligences artificielles de génération de code ! Et oui, ce projet a été en partie générée par deux intelligences artificielles. D'une part chatgpt, un outil qui certes vous ce qui vaut, pas génial à mon goût pour de la programmation, mais qui retourne quand même quelques petits éléments intéressants. Il ne faut pas avoir peur de corriger le code qui retourne, mais cela peut-être une bonne base de départ.
À côté de cela, j'utilise l'autre intelligence artificielle, Madame "Claude". En effet, cette intelligence artificielle est plus spécialisée dans le développement et la production de code à partir d'une phrase de description d'un projet. De plus, elle retourne pas mal de choses intéressantes, le mix des deux est plutôt un projet intéressant. Toutefois, dans les deux cas, mes demandes explicites de commenter le code ne sont pas brillantes !
Un unique point de départ
Pour concevoir cette synthèse, je suis parti d’un élément de base particulièrement simple : un fichier au format GPX (GPS Exchange Format). Ce fichier, généré lors d’une activité de marche ou d’un enregistrement GPS, contient uniquement les données brutes du parcours — coordonnées géographiques, altitudes, points de passage, et horodatages.
À partir de ce seul fichier, j’ai demandé a chatgpt de mettre en place un traitement automatique permettant d’extraire, calculer et présenter toutes les informations visibles sur la capture d’écran : distance totale parcourue, durée de l’itinéraire, vitesse moyenne, dénivelé cumulé, profils altimétriques, ainsi que les différentes étapes du trajet. Autrement dit, toute la richesse de cette synthèse repose sur l’exploitation intelligente de ce fichier unique.

Mon besoin
Comme beaucoup d'informaticiens, vous me savez donc très fainéant. Mon idée était de pouvoir donner en pâture un fichier GPX afin qu'il me ressorte automatiquement l'ensemble des informations que, jusqu'à présent, je recopiais à partir d'outils de cartographie très connu sur le web, afin de formaliser un article Joomla.
Je voulais bien entendu que ces articles jumia deviennent de plus en plus automatisés.
Le projet n'est pas d'attaquer les API de Joomla afin d'insérer un article automatiquement, mais de me proposer un brouillon que je puisse copier-coller, afin de pouvoir éventuellement apporter ma touche personnelle, avant de publier mon article.
Que fait alors mon projet ?
Dans ce paragraphe, je ne vais pas détailler l'ensemble du code des classes fournies par le projet. Leur nom et largement explicite afin de comprendre ce qu'elles font. Cependant, mon projet utilise la classe présentée, et après avoir instancier cette classe, je parse le fichier GPX au moyen de la méthode parseGPX().
Lorsque le fichier a subi ce traitement, le code me retourne un objet issu de l'instanciation de la classe permettant de traitement du fichier, et me retourne un objet $gpx.
Cet objet contient une méthode, renderLeafletMap(), donc le but est d'afficher la carte au moyen de l'outil OpenStreetMap, du tracé effectué au travers du fichier GPX.
A la suite de cette opération, une méthode getstatistics() permet de me retourner sous la forme de statistiques, toute une série de métadonnées issues du fichier GPX.
J'enchaîne la récupération des données au travers de la méthode generateInstructions() dont le but est de lire dans le contenu du fichier, l'ensemble des instructions d'actions à effectuer afin de suivre le tracé GPX. En l'occurrence, nous retrouvons les instructions pour tourner à gauche tourner à droite allez tout droit arrivée à destination etc...
La classe GPX fournie par le générateur de code Claude a aussi généré une deuxième classe sur le même principe que la précédente, mais dont le but est essentiellement dans un affichage des instructions sur une page web. Ainsi, j'appelle cette deuxième classe afin de pouvoir afficher ses instructions avec l'ensemble des balises <div> et <span> permettant d'appliquer une feuille de style.
À la suite de ces instructions, j'ai aussi demandé à Claude de me générer une méthode qui me permette d'extraire l'ensemble des métadonnées qui sont stockés dans le fichier au format GPX. Ainsi, j'appelle aussi cette méthode qui va m'afficher ces informations. Vous remarquerez que cette méthode a le suffixe HTML, pour la même raison, elle me retourne un code html propre sur lequel je vais pouvoir appliquer une feuille de style là aussi.
La dernière méthode Closestpoint permet de trouver le point le plus proche afin de pouvoir à partir de ce dernier calculer des distances. Encore une fois, je n'ai pas regardé en détail le code généré par Claude.
Nul en math
L'utilisation de Claude est en lien très étroit avec ma nullité mathématique (et oui, on peut être titulaire d'un BTS Informatique réseau avec 02 en math !). En effet, il est possible de calculer l'ensemble d'un parcours, en terme de distance, avec une suite de points GPS.
Cependant, après avoir regardé la formule mathématique, cette dernière fait appel a des conversions de degrés en radian, afin de pouvoir appliquer des sinus et des cosinus, des tangentes, des racines carrées. Même si je sais ce qu'est tout ça, j'ai vu ça au collège quand même, je suis bien incapable de sortir ses calculs maintenant thématique pour calculer ce genre de distance. Utilisation d'une intelligence artificielle dans ce cas est quand même fort pratique, ça évite d'aller embêter et copains ingénieurs.
Formatage
La dernière étape que j'ai effectué, qui même si elle n'est pas très compliquée pose quelques soucis quand nous n'avons aucune formation infographiste, a été à mise en page du résultat. En effet, même si Claude retourne du CSS joli et propre, il est loin d'être optimal et agréable. J'ai donc passé comme dernière étape la reprise du CSS qui a été généré, j'ai rajouté quelques classes et id ici et là afin d'appliquer un style propre et correct à mon résultat.
Parmi les éléments de mise en page, vous voyez que j'ai mis côte à côte l'ensemble de trois blocs, il y a
- la carte
- les informations GPX
- ainsi que les instructions de navigation, ce qui peut largement être discutable.
Notamment, lorsque vous souhaitez appliquer comme je l'ai fait il y a la possibilité d'imprimer le résultat au format PDF, au travers d'une bibliothèque JavaScript (jsPdf). Et l'application du style et de la mise en place des divisions est vraiment très importante.
Je reconnais, c'est une des rares limites du code générer par Claude, cela dit, comme je n'avais pas pensé à l'impression au moment où j'ai commencé mon projet, je n'ai rien dit à Claude sur ses possibilités d'impression en PDF. Cela dit, je ne suis pas trop mécontent du résultat final.
La classe :
PHP
<html>
<head>
<link href="/style.css" rel="stylesheet" />
</head>
<body>
<div class="conteneur">
<div id="pgg">dvdvfzsgfsg</div>
<?php
include("GPX4.claude.class.php");
$gpx = new GPXProcessor();
//include("pdfentete.php");
$gpx->parseGPX('Thionville.gpx');
//echo "<h1>".$gpx->extractMetadata('Thionville.gpx')."</h1>";
echo $gpx->renderLeafletMap();
// Obtenir les statistiques
$stats = $gpx->getStatistics();
// Générer les instructions
$instructions = $gpx->generateInstructions();
echo $gpx->getFormattedInstructions();
// Afficher les métadonnées
echo $gpx->getMetadataHTML();
// Trouver le point le plus proche
$closest = $gpx->getClosestPoint(48.5734, 7.7521);
?>
<!-- Bouton -->
<button id="downloadPdfBtn">Télécharger en PDF</button>
<!-- Page de contenu -->
<div id="cover-page" >
<h1>Rapport GPX</h1>
<p id="madate">Date : <span id="date-placeholder"></span></p>
<p>Document généré automatiquement</p>
</div>
<div id="main-content">
<h1>Contenu principal</h1>
<p>Voici les détails du parcours GPX...</p>
</div>
<!-- Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script>
document.getElementById("downloadPdfBtn").addEventListener("click", async function () {
// Préparer la date
document.getElementById("date-placeholder").textContent = new Date().toLocaleDateString();
const { jsPDF } = window.jspdf;
const pdf = new jsPDF({
unit: "mm",
format: "a4",
orientation: "portrait"
});
// Fonction utilitaire pour ajouter une capture d'un élément DOM
async function addPageFromElement(element, addNewPage = false) {
const canvas = await html2canvas(element, {
scale: 2,
useCORS: true
});
const imgData = canvas.toDataURL("image/jpeg", 1.0);
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
if (addNewPage) pdf.addPage();
pdf.addImage(imgData, 'JPEG', 0, 0, pageWidth, pageHeight);
}
// Affiche la page de garde temporairement
const coverPage = document.getElementById("cover-page");
coverPage.style.display = "block";
// Ajoute la page de garde
await addPageFromElement(coverPage);
// Masque la page de garde (nettoyage visuel)
coverPage.style.display = "none";
// Ajoute le contenu principal (dans une nouvelle page)
await addPageFromElement(document.getElementById("main-content"), true);
// Télécharge le PDF
pdf.save("rapport-gpx.pdf");
});
function imprimer(){
alert ("Penser à vérifier dans l'aperçu, le positionnement de la trace ! Elle doit entre en haut a gauche du cadre pour que ca marche ");
window.print();
}
</script>
<p style="text-align:center; margin-top: 2em;">
<button onclick="imprimer();">📄 Imprimer ou enregistrer en PDF</button>
</p>
</div>
</body>
</html>
CSS
.gpx-navigation {
display: block;
float: right;
max-width: 100%;
overflow-y: scroll;
height: 100%;
}
.gpx-metadata h3 {
text-align: center;
color: red;
border: 2px solid blue;
left: 20%;
background-color: yellow;
width: 30%;
width: 15%;
margin-left: 51%;
}
#idee {
display: none;
}
#map { height: 100%;width:50%;float:left; margin-right: 1%;}
.nav-step:nth-child(odd) {
background-color: aliceblue; /* couleur pour les étapes impaires */
}
.nav-step:nth-child(even) {
background-color: lightyellow; /* couleur pour les étapes paires */
}
/* Optionnel : un peu d'espacement et de padding pour la lisibilité */
.nav-step {
/* padding: 10px;*/
margin-bottom: 5px;
border-radius: 4px;
}
#cover-page {
display: none;
}
h1{
display:none;}
#downloadPdfBtn{
display:none;
}
.conteneur #pgg{display:none}
/* ----------------------------------------------------------------------------------------------------------------- */
@media print {
#cover-page {
display: block;
}
#pgg{page-break-after: always;}
#idee{display:block;}
.conteneur #pgg{display:block;}
#cover-page #madate{
display: none;
}
.info-section h4, .stats-section h4{
text-align:center;
font-weight:bold;
border:2px solid black;
}
H1{
display: block;
width:100%;
text-align:center;
}
/* Masquer seulement le titre h3 du bloc */
.gpx-metadata h3 {
text-align: center;
color: red;
border: 2px solid blue;
left: 20%;
background-color: yellow;
width: 30%;
width: 15%;
margin-left: 51%;
display: none;
}
#downloadPdfBtn{
display:none;
}
.gpx-metadata{
order:2;
margin-top:40%;
}
/* Masquer les éléments inutiles dans la version PDF */
button,
nav,
footer,
.no-print {
display: none !important;
}
/* Optimiser les marges de la page */
body {
margin: 1.5cm;
font-size: 12pt;
line-height: 1.5;
color: #000;
background: none;
}
.conteneur{
display: flex;
flex-direction: column;
}
/* Forcer les sauts de page entre sections si besoin */
.page-break {
page-break-before: always;
}
#main-content{
page-break-after: always;
/*display:none;*/
}
html body div#main-content h1{display:none;}
html body div#main-content p{display:none;}
/*html body h1{display:none;}*/
/* Étendre les éléments plein écran comme les cartes */
#map,
.leaflet-container {
height: 900px !important;
width: 100% !important;
}
/* Supprimer les effets visuels non imprimables */
a {
color: black;
text-decoration: none;
}
/* Afficher les URL à côté des liens (facultatif) */
a::after {
content: " (" attr(href) ")";
font-size: 10pt;
}
.gpx-navigation {
/* S’assure que le bloc soit visible dans sa totalité */
overflow: visible !important;
height: auto !important;
max-height: none !important;
page-break-inside: avoid; /* évite que la navigation soit coupée en deux pages */
order:3;
}
.gpx-table{
display:block !important;
}
/* Pour éviter les coupures en général sur les blocs */
section, article, div {
page-break-inside: avoid;
}
.leaflet-control-zoom {
display:none;
}
.nav-step:nth-child(2n) {
background-color: lightyellow;
}
.nav-step:nth-child(2n+1) {
background-color: aliceblue;
}
html body div#map.leaflet-container.leaflet-touch.leaflet-fade-anim.leaflet-grab.leaflet-touch-drag.leaflet-touch-zoom div.leaflet-control-container div.leaflet-bottom.leaflet-right div.leaflet-control-attribution.leaflet-control
{
display:none;
}
.leaflet-container a.leaflet-popup-close-button{display:none;}
}
La classe
<?php
/**
* Classe GPX unifiée pour parser, analyser et générer des instructions de navigation
* à partir de fichiers GPX
*
* @author Claude Assistant
* @version 2.0
*/
class GPXProcessor
{
private $trackpoints = [];
private $metadata = [];
private $instructions = [];
// Configuration des seuils
private $minDistanceForInstruction = 10; // mètres
private $angleThreshold = 30; // degrés pour détecter un changement de direction
private $speedThreshold = 1.0; // m/s pour détecter les arrêts
private $walkingSpeed = 83.33; // mètres par minute (5 km/h)
/**
* Constructeur
* @param string|null $gpxFile Chemin vers le fichier GPX (optionnel)
*/
public function __construct($gpxFile = null)
{
if ($gpxFile) {
$this->parseGPX($gpxFile);
}
}
/**
* Parse le fichier GPX et extrait les trackpoints et métadonnées
* @param string $gpxFile Chemin vers le fichier GPX
* @return int Nombre de points extraits
* @throws Exception Si le fichier n'existe pas ou ne peut être parsé
*/
public function parseGPX($gpxFile)
{
if (!file_exists($gpxFile)) {
throw new Exception("Fichier GPX non trouvé: $gpxFile");
}
$xml = simplexml_load_file($gpxFile);
if (!$xml) {
throw new Exception("Impossible de parser le fichier GPX");
}
// Gestion des namespaces GPX
$xml->registerXPathNamespace('gpx', 'http://www.topografix.com/GPX/1/1');
// Extraction des métadonnées
$this->extractMetadata($xml);
// Réinitialisation des trackpoints
$this->trackpoints = [];
// Extraction des points de trace (trkpt) - priorité
$trackPoints = $xml->xpath('//gpx:trkpt');
if (empty($trackPoints)) {
// Fallback sur les segments de track classiques
foreach ($xml->trk as $track) {
foreach ($track->trkseg as $segment) {
foreach ($segment->trkpt as $point) {
$this->addTrackpoint($point);
}
}
}
} else {
foreach ($trackPoints as $point) {
$this->addTrackpoint($point);
}
}
// Si toujours pas de trackpoints, essayer les waypoints
if (empty($this->trackpoints)) {
$waypoints = $xml->xpath('//gpx:wpt');
foreach ($waypoints as $point) {
$this->addTrackpoint($point);
}
}
if (empty($this->trackpoints)) {
throw new Exception("Aucun trackpoint trouvé dans le fichier GPX");
}
return count($this->trackpoints);
}
/**
* Ajoute un trackpoint à la liste
* @param SimpleXMLElement $point Point XML
*/
private function addTrackpoint($point)
{
$this->trackpoints[] = [
'lat' => (float)$point['lat'],
'lon' => (float)$point['lon'],
'ele' => isset($point->ele) ? (float)$point->ele : null,
'time' => isset($point->time) ? (string)$point->time : null
];
}
/**
* Extrait les métadonnées du fichier GPX
* @param SimpleXMLElement $xml Document XML
*/
private function extractMetadata($xml)
{
// Métadonnées de base
$this->metadata = [
'version' => (string)($xml['version'] ?? 'N/A'),
'creator' => (string)($xml['creator'] ?? 'N/A'),
'name' => isset($xml->metadata->name) ? (string)$xml->metadata->name : 'N/A',
'desc' => isset($xml->metadata->desc) ? (string)$xml->metadata->desc : 'N/A',
'author' => isset($xml->metadata->author->name) ? (string)$xml->metadata->author->name : 'N/A',
'time' => isset($xml->metadata->time) ? (string)$xml->metadata->time : 'N/A',
'keywords' => isset($xml->metadata->keywords) ? (string)$xml->metadata->keywords : 'N/A'
];
// Limites géographiques
if (isset($xml->metadata->bounds)) {
$this->metadata['bounds'] = [
'minlat' => (string)$xml->metadata->bounds['minlat'],
'minlon' => (string)$xml->metadata->bounds['minlon'],
'maxlat' => (string)$xml->metadata->bounds['maxlat'],
'maxlon' => (string)$xml->metadata->bounds['maxlon']
];
}
// Informations sur la trace
$tracks = $xml->xpath('//gpx:trk');
if (!empty($tracks)) {
$track = $tracks[0];
$this->metadata['track_name'] = isset($track->name) ? (string)$track->name : 'N/A';
$this->metadata['track_desc'] = isset($track->desc) ? (string)$track->desc : 'N/A';
$this->metadata['track_type'] = isset($track->type) ? (string)$track->type : 'N/A';
$segments = $track->xpath('.//gpx:trkseg');
$this->metadata['segments_count'] = count($segments);
}
// Compteurs
$this->metadata['waypoints_count'] = count($xml->xpath('//gpx:wpt'));
$this->metadata['routes_count'] = count($xml->xpath('//gpx:rte'));
}
/**
* Calcule la distance entre deux points (formule de Haversine)
* @param array $point1 Premier point [lat, lon]
* @param array $point2 Deuxième point [lat, lon]
* @return float Distance en mètres
*/
public function calculateDistance($point1, $point2)
{
$earthRadius = 6371000; // Rayon de la Terre en mètres
$lat1 = deg2rad($point1['lat']);
$lat2 = deg2rad($point2['lat']);
$deltaLat = deg2rad($point2['lat'] - $point1['lat']);
$deltaLon = deg2rad($point2['lon'] - $point1['lon']);
$a = sin($deltaLat/2) * sin($deltaLat/2) +
cos($lat1) * cos($lat2) *
sin($deltaLon/2) * sin($deltaLon/2);
$c = 2 * atan2(sqrt($a), sqrt(1-$a));
return $earthRadius * $c;
}
/**
* Calcule le bearing (direction) entre deux points
* @param array $point1 Premier point
* @param array $point2 Deuxième point
* @return float Bearing en degrés (0-360)
*/
private function calculateBearing($point1, $point2)
{
$lat1 = deg2rad($point1['lat']);
$lat2 = deg2rad($point2['lat']);
$deltaLon = deg2rad($point2['lon'] - $point1['lon']);
$y = sin($deltaLon) * cos($lat2);
$x = cos($lat1) * sin($lat2) - sin($lat1) * cos($lat2) * cos($deltaLon);
$bearing = rad2deg(atan2($y, $x));
return ($bearing + 360) % 360;
}
/**
* Convertit un bearing en direction cardinale
* @param float $bearing Bearing en degrés
* @return string Direction cardinale
*/
private function bearingToDirection($bearing)
{
$directions = [
'Nord', 'Nord-Est', 'Est', 'Sud-Est',
'Sud', 'Sud-Ouest', 'Ouest', 'Nord-Ouest'
];
$index = round($bearing / 45) % 8;
return $directions[$index];
}
/**
* Détermine le type de virage basé sur la différence d'angle
* @param float $angleDiff Différence d'angle
* @return string Instruction de virage
*/
private function getTurnInstruction($angleDiff)
{
$absAngle = abs($angleDiff);
if ($absAngle < $this->angleThreshold) {
return "Continuez tout droit";
} elseif ($absAngle < 60) {
return $angleDiff > 0 ? "Tournez légèrement à droite" : "Tournez légèrement à gauche";
} elseif ($absAngle < 120) {
return $angleDiff > 0 ? "Tournez à droite" : "Tournez à gauche";
} elseif ($absAngle < 160) {
return $angleDiff > 0 ? "Tournez franchement à droite" : "Tournez franchement à gauche";
} else {
return "Faites demi-tour";
}
}
/**
* Formate la distance en unité appropriée
* @param float $meters Distance en mètres
* @return string Distance formatée
*/
public static function formatDistance($meters)
{
if ($meters < 1000) {
return round($meters) . " mètres";
} else {
return round($meters / 1000, 1) . " kilomètres";
}
}
/**
* Génère les instructions de navigation
* @return array Instructions de navigation
*/
public function generateInstructions()
{
if (count($this->trackpoints) < 2) {
return ["Pas assez de points pour générer des instructions"];
}
$this->instructions = [];
$cumulativeDistance = 0;
$segmentDistance = 0;
$currentBearing = null;
// Point de départ
$startPoint = $this->trackpoints[0];
$this->instructions[] = [
'type' => 'start',
'text' => "Départ de votre itinéraire",
'distance' => 0,
'cumulative_distance' => 0,
'coordinates' => $startPoint['lat'] . ', ' . $startPoint['lon']
];
for ($i = 1; $i < count($this->trackpoints); $i++) {
$previousPoint = $this->trackpoints[$i-1];
$currentPoint = $this->trackpoints[$i];
$distance = $this->calculateDistance($previousPoint, $currentPoint);
$bearing = $this->calculateBearing($previousPoint, $currentPoint);
$cumulativeDistance += $distance;
$segmentDistance += $distance;
// Détecter un changement de direction significatif
if ($currentBearing !== null) {
$angleDiff = $bearing - $currentBearing;
// Normaliser la différence d'angle
if ($angleDiff > 180) $angleDiff -= 360;
if ($angleDiff < -180) $angleDiff += 360;
// Si changement de direction significatif et distance suffisante
if (abs($angleDiff) > $this->angleThreshold && $segmentDistance > $this->minDistanceForInstruction) {
// Ajouter instruction pour le segment précédent
$direction = $this->bearingToDirection($currentBearing);
$turnInstruction = $this->getTurnInstruction($angleDiff);
$this->instructions[] = [
'type' => 'segment',
'text' => "Marchez " . self::formatDistance($segmentDistance) . " en direction du $direction",
'distance' => $segmentDistance,
'cumulative_distance' => $cumulativeDistance - $distance,
'coordinates' => $previousPoint['lat'] . ', ' . $previousPoint['lon']
];
$this->instructions[] = [
'type' => 'turn',
'text' => $turnInstruction,
'distance' => 0,
'cumulative_distance' => $cumulativeDistance - $distance,
'coordinates' => $currentPoint['lat'] . ', ' . $currentPoint['lon']
];
$segmentDistance = $distance;
}
}
$currentBearing = $bearing;
}
// Dernier segment
if ($segmentDistance > $this->minDistanceForInstruction) {
$direction = $this->bearingToDirection($currentBearing);
$this->instructions[] = [
'type' => 'segment',
'text' => "Marchez " . self::formatDistance($segmentDistance) . " en direction du $direction",
'distance' => $segmentDistance,
'cumulative_distance' => $cumulativeDistance,
'coordinates' => end($this->trackpoints)['lat'] . ', ' . end($this->trackpoints)['lon']
];
}
// Point d'arrivée
$endPoint = end($this->trackpoints);
$this->instructions[] = [
'type' => 'end',
'text' => "Arrivée à destination",
'distance' => 0,
'cumulative_distance' => $cumulativeDistance,
'coordinates' => $endPoint['lat'] . ', ' . $endPoint['lon']
];
return $this->instructions;
}
public function getIBPLevel() {
$stats = $this->getStatistics();
$distance_km = $stats['distance'] / 1000; // convertir en kilomètres
$denivele_positif = isset($stats['ascent']) ? $stats['ascent'] : 0;
$denivele_negatif = isset($stats['descent']) ? $stats['descent'] : 0;
// Formule approximative pour calculer le niveau IBP
$ibp = round(
($distance_km * 1.5) +
($denivele_positif / 10) +
($denivele_negatif / 20)
);
$stats['ibp'] = $ibp;
return $ibp;
}
/**
* Calcule la distance totale du parcours
* @return float Distance totale en mètres
*/
public function getTotalDistance()
{
if (count($this->trackpoints) < 2) {
return 0;
}
$totalDistance = 0;
for ($i = 1; $i < count($this->trackpoints); $i++) {
$totalDistance += $this->calculateDistance($this->trackpoints[$i-1], $this->trackpoints[$i]);
}
return $totalDistance;
}
/**
* Calcule le dénivelé positif
* @return float Dénivelé en mètres
*/
public function getElevationGain()
{
$gain = 0;
$previousElevation = null;
foreach ($this->trackpoints as $point) {
if (isset($point['ele']) && $previousElevation !== null) {
$diff = $point['ele'] - $previousElevation;
if ($diff > 0) {
$gain += $diff;
}
}
if (isset($point['ele'])) {
$previousElevation = $point['ele'];
}
}
return $gain;
}
/**
* Calcule le temps estimé de marche
* @return int Temps en minutes
*/
public function getEstimatedWalkingTime()
{
$distance = $this->getTotalDistance();
if ($distance == 0) return 0;
// Temps de base pour la distance horizontale
$baseTime = $distance / $this->walkingSpeed;
// Ajout du temps pour le dénivelé (règle de Naismith)
$elevationGain = $this->getElevationGain();
$elevationTime = $elevationGain / 10; // 1 minute par 10m de dénivelé
return round($baseTime + $elevationTime);
}
/**
* Formate le temps en format lisible
* @param int $minutes Temps en minutes
* @return string Temps formaté
*/
public static function formatWalkingTime($minutes)
{
if ($minutes < 60) {
return $minutes . ' min';
}
$hours = floor($minutes / 60);
$remainingMinutes = $minutes % 60;
if ($remainingMinutes == 0) {
return $hours . 'h';
}
return $hours . 'h' . sprintf('%02d', $remainingMinutes);
}
/**
* Trouve le point le plus proche d'une coordonnée donnée
* @param float $refLat Latitude de référence
* @param float $refLon Longitude de référence
* @return array|null Point le plus proche avec sa distance
*/
public function getClosestPoint($refLat, $refLon)
{
if (empty($this->trackpoints)) {
return null;
}
$minDistance = PHP_FLOAT_MAX;
$closestPoint = null;
foreach ($this->trackpoints as $index => $point) {
$refPoint = ['lat' => $refLat, 'lon' => $refLon];
$distance = $this->calculateDistance($refPoint, $point);
if ($distance < $minDistance) {
$minDistance = $distance;
$closestPoint = [
'index' => $index,
'coordinate' => $point,
'distance' => $distance
];
}
}
return $closestPoint;
}
/**
* Retourne les statistiques complètes du parcours
* @return array Statistiques
*/
public function getStatistics()
{
$stats = [
'total_points' => count($this->trackpoints),
'total_distance' => $this->getTotalDistance(),
'formatted_distance' => self::formatDistance($this->getTotalDistance()),
'estimated_time' => $this->getEstimatedWalkingTime(),
'formatted_time' => self::formatWalkingTime($this->getEstimatedWalkingTime()),
'elevation_gain' => $this->getElevationGain()
];
if (!empty($this->trackpoints)) {
$stats['start_point'] = $this->trackpoints[0];
$stats['end_point'] = end($this->trackpoints);
}
// Statistiques d'élévation si disponibles
$elevations = array_filter(array_column($this->trackpoints, 'ele'));
if (!empty($elevations)) {
$stats['min_elevation'] = min($elevations);
$stats['max_elevation'] = max($elevations);
}
return $stats;
}
/**
* Retourne les instructions formatées en HTML
* @return string Instructions HTML
*/
public function getFormattedInstructions()
{
if (empty($this->instructions)) {
$this->generateInstructions();
}
$output = "<div class='gpx-navigation'>";
$output .= "<h3>Instructions de Navigation</h3>";
foreach ($this->instructions as $index => $instruction) {
$step = $index + 1;
$output .= "<div class='nav-step nav-{$instruction['type']}'>";
$output .= "<strong>Étape $step:</strong> " . htmlspecialchars($instruction['text']);
if ($instruction['distance'] > 0) {
$output .= " <em>(" . self::formatDistance($instruction['distance']) . ")</em>";
}
if ($instruction['cumulative_distance'] > 0) {
$output .= " <small>[Total: " . self::formatDistance($instruction['cumulative_distance']) . "]</small>";
}
$output .= "</div>";
}
$output .= "</div>";
return $output;
}
public function getTitleOnly()
{
if (empty($this->metadata) || empty($this->metadata['name']) || $this->metadata['name'] === 'N/A') {
return '<h1>Aucun titre disponible</h1>';
}
return '<h1>' . htmlspecialchars($this->metadata['name']) . '</h1>';
}
/**
* Retourne les métadonnées formatées en HTML
* @return string Métadonnées HTML
*/
public function getMetadataHTML()
{
if (empty($this->metadata)) {
return '<div class="gpx-metadata"><p>Aucune métadonnée disponible</p></div>';
}
$stats = $this->getStatistics();
$html = '<div class="gpx-metadata">';
$html .= '<h3>Informations du parcours GPX</h3>';
// Informations générales
$html .= '<div class="info-section">';
$html .= '<h4>Détails généraux</h4>';
$html .= '<table class="gpx-table">';
$fields = [
'Nom' => $this->metadata['name'],
'Description' => $this->metadata['desc'],
'Créateur' => $this->metadata['creator'],
'Version' => $this->metadata['version'],
'Date de création' => $this->formatDate($this->metadata['time'])
];
foreach ($fields as $label => $value) {
if ($value !== 'N/A' && !empty($value)) {
$html .= "<tr><td><strong>$label:</strong></td><td>" . htmlspecialchars($value) . "</td></tr>";
}
}
$html .= '</table></div>';
// Statistiques du parcours
$html .= '<div class="stats-section">';
$html .= '<h4>Statistiques</h4>';
$html .= '<table class="gpx-table">';
$html .= "<tr><td><strong>Distance totale:</strong></td><td>{$stats['formatted_distance']}</td></tr>";
$html .= "<tr><td><strong>Temps estimé:</strong></td><td>{$stats['formatted_time']}</td></tr>";
$html .= "<tr><td><strong>Nombre de points:</strong></td><td>{$stats['total_points']}</td></tr>";
$html .= "<tr><td><strong>Dénivelé positif:</strong></td><td>" . round($stats['elevation_gain']) . " m</td></tr>";
$html .= '</table></div>';
$html .= '</div>';
return $html;
}
/**
* Formate une date ISO en format lisible
* @param string $dateString Date ISO
* @return string Date formatée
*/
private function formatDate($dateString)
{
if ($dateString === 'N/A' || empty($dateString)) {
return 'N/A';
}
try {
$date = new DateTime($dateString);
return $date->format('d/m/Y H:i:s');
} catch (Exception $e) {
return $dateString;
}
}
/**
* Exporte les données en JSON
* @return string JSON
*/
public function exportToJSON()
{
return json_encode([
'metadata' => $this->metadata,
'trackpoints' => $this->trackpoints,
'statistics' => $this->getStatistics(),
'instructions' => $this->instructions
], JSON_PRETTY_PRINT);
}
/**
* Génère une carte Leaflet contenant la trace GPX
* @return string Code HTML à insérer dans une page web
*/
public function renderLeafletMap()
{
if (empty($this->trackpoints)) {
return '<p>Aucun point à afficher sur la carte.</p>';
}
// Centrage de la carte sur le premier point
$startLat = $this->trackpoints[0]['lat'];
$startLon = $this->trackpoints[0]['lon'];
// Construction du tableau JS des points
$coords = array_map(function($pt) {
return '[' . $pt['lat'] . ', ' . $pt['lon'] . ']';
}, $this->trackpoints);
$jsArray = '[' . implode(',', $coords) . ']';
// HTML + JS Leaflet
return <<<HTML
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<div id="map"></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const map = L.map('map').setView([$startLat, $startLon], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
const latlngs = $jsArray;
const polyline = L.polyline(latlngs, {color: 'blue'}).addTo(map);
map.fitBounds(polyline.getBounds());
// Ajout du point de départ
L.marker(latlngs[0]).addTo(map).bindPopup("Départ").openPopup();
</script>
HTML;
}
/**
* Getters
*/
public function getTrackpoints() { return $this->trackpoints; }
public function getMetadata() { return $this->metadata; }
public function getInstructions() { return $this->instructions; }
/**
* Setters pour la configuration
*/
public function setMinDistanceForInstruction($distance) { $this->minDistanceForInstruction = $distance; }
public function setAngleThreshold($angle) { $this->angleThreshold = $angle; }
public function setWalkingSpeed($speed) { $this->walkingSpeed = $speed; }
}
?>

