GPS : conversion NMEA vers KML

Tout le monde connait GoogleEarth et ses dérivés, tout le monde sait que leur prédilection est l’utilisation de fichier kml (ou gpx)

Certains savent que googlemap permet d’afficher un trajet à partir d’un fichier kml (ou gpx).

Si vous avez créé votre propre GPS data logger (ou si vous en avez acheté un dans le commerce), vous avez certainement remarqués que les données sont fournis en NMEA !

KML,NMEA : quesaco ?

Le standard KML  (voir Wikipédia) est un standard pour stocker les données cartographique dans une syntaxe XML(c’est un fichier texte dont le nom des variables et leur valeurs sont entre des tag), vous pouvez en ouvrir un avec le bloc notes de windows.

un exemple ici (extraits)

<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<name>KML Samples</name>
<open>1</open>
<description>Unleash your creativity with the help of these examples!</description>
<Style id="downArrowIcon">
<IconStyle>
<Icon>
<href>http://maps.google.com/mapfiles/kml/pal4/icon28.png</href>
</Icon>
</IconStyle>
............................
............................
............................
<Folder>
<name>Placemarks</name>
<description>These are just some of the different kinds of placemarks with
which you can mark your favorite places</description>
<LookAt>
<longitude>-122.0839597145766</longitude>
<latitude>37.42222904525232</latitude>
<altitude>0</altitude>
<heading>-148.4122922628044</heading>
<tilt>40.5575073395506</tilt>
<range>500.6566641072245</range>
</LookAt>
............................
............................
............................
<LookAt>
<longitude>-112.0822680013139</longitude>
<latitude>36.09825589333556</latitude>
<altitude>0</altitude>
<heading>103.8120432044965</heading>
<tilt>62.04855796276328</tilt>
<range>2889.145007690472</range>
</LookAt>
............................
............................
............................
</Folder>
............................
............................
............................
</Document>
</kml>

 

Le standard NMEA  (voir ici) est également un standard pour stocker les données cartographique mais dans une syntaxe CSV (tableau dont les colonnes sont séparées par des virgules) vous pouvez en ouvrir un avec le bloc notes de windows.

Entre les deux : une différence de taille :

  • le NMEA permet de stocker les informations au fil de l’eau, dans l’ordre dans lesquels elles arrivent. C’est un fichier « brute de fonderie », qui est enrichi ligne à ligne au fur et à mesure du trajet réalisé.
  • En KML, dès le début du fichier, on s’aperçoit qu’il faut préciser des valeurs qu’on ne connait pas encore tant qu’on n’a pas lu l’intégralité des données du  trajet (en gros on ne peut pas créer un fichier KML au fil de l’eau :  on ne peut le faire qu’une fois que le trajet est tout à fait terminé).

Un exemple ici :

$GPRMC,053740.000,A,2503.6319,N,12136.0099,E,2.69,79.65,100106,,,A*53
..................
..................
$GPGGA,064036.289,4836.5375,N,00740.9373,E,1,04,3.2,200.2,M,,,,0000*0E

Le standard NMEA est extrêmement bien documenté sur internet. Tout y est dit et bien qu’il y est beaucoup d’informations, toutes sont aisément compréhensibles.

Le standard KML est également documenté mais c’est franchement « fouillis » et surtout c’est un standard destiné à évoluer (c’est sa force et sa faiblesse) : on ne sait jamais si la doc qu’on lit est la dernière version.

Je parlais de Web, ben oui, l’idée étant d’afficher sur mon site web, via googlemap, le trajet que mon datalogger a enregistré en NMEA.

Arghh, donc il faut convertir le NMEA en KML (d’où le titre), cherchez pas sur le web, il n’y a rien  que j’ai trouvé au moment où j’écris ces lignes.

Donc comme je me dis que ce que j’ai écris en code, j’en aurai besoin de nouveau dans quelques années ,que je n’ai pas de tête et abandonné les cahiers depuis longtemps, le cahier s’est transformé en blog.

Voici donc une fonction qui réalise la conversion, écrite en php

Avant que certains geeks (dont certains ne savent pas trier un tableau ou faire une boucle…) ne se mettent à râler sachez que si elle n’utilise pas d’écriture condensée, c’est pour que tout le monde comprenne, y compris le lycée qui passionné débute le soir à la maison ou le retraiter qui vient juste de s’y mettre.

Quelques précisions : KML attend des date-heure au format UTC (heure universelle), ça tombe bien c’est ce que nous donne le NMEA

Mais KML attend des date-heure au format YYYY-MM-DDTHH:MM:SSZ tandis que le NMEA nous donne dans un champs une date au format DDMMAA puis dans un autre, une heure au format HHMMSS.CC, il doit donc y avoir conversion d’un format à l’autre.

C’est le rôle de cette fonction :

//entrée 030819 125102.00, sortie 2019-08-03T12:51:02Z
function nmeadatetime_to_kmldatetime($date,$time)
{
$aa="20".substr($date,4,2);
$mm=substr($date,2,2);
$dd=substr($date,0,2);
$hr=substr($time,0,2);
$mn=substr($time,2,2);
$ss=substr($time,4,2);
$s=$aa."-".$mm."-".$dd." ".$hr.":".$mn.":".$ss;
//echo $s;
if (DateTime::createFromFormat('Y-m-d H:i:s', $s) !== FALSE) 
{ return $aa."-".$mm."-".$dd."T".$hr.":".$mn.":".$ss."Z"; } 
else { return ""; }
}

Autre problème : KML attend des coordonnées géographique signées (Lat Sud  ou Long Ouest => valeurs négatives) et au format décimal tandis qu’en NMEA on obtient cela en plusieurs champs :

  • L’un donne le sens (N = Nord, S = Sude, E = Est, W= Ouest)
  • L’autre la valeur en degrés + minutes et décimales de minute (c’est quoi se truc ?? qui a inventé ça ?? ça pourrait pas être des minutes secondes !!) bref

Il faut donc encore une conversion , c’est le rôle de cette autre fonction (qui suppose qu’on a déjà mis le signe)

//entrée 4807.038 ou -4807.038 vers 48.102453 (07.038 sont des minutes)
function nmeadeg_to_kmldeg($deg)
{
$sens=1;
$result="";
//echo($deg."<br>"); 
$t=explode(".",$deg);
if (count($t)==2)
{
$dd=$t[0];
if (substr($dd,0,1)=="-") {$sens=-1; $dd=substr($dd,1);}
$dd=floor($dd/100);
//echo($dd."<br>"); 
$mm=($t[0]-100*$dd).".".$t[1];
//echo($mm."<br>"); 
$dddec=ceil(10000*100*$mm/60)/10000;
//echo($dddec."<br>"); 
$result=$sens*($dd+$dddec/100);
}
return $result;
}

Autre problème : je n’utilise pas ces données pour un véhicule terrestre mais pour un Aéroplane, donc je veux l’altitude, elle est fournis par deux champs :

  • L’un donne la valeur
  • L’autre l’unité (M = Mètres, F = Feet ou Pieds,… ah ces Ango-Saxons ….)

Mais KML veut cela en mètres , donc nouvelle fonction de conversion que voici

//entrée M4803, sortie : ALT en mettre
function nmeaalt_to_kmlalt($alt)
{
$result="";
//echo($deg."<br>"); 
if (substr($alt,0,1)=="M") $s=substr($alt,1);
elseif (substr($alt,0,1)=="F") $s=substr($alt,1)*0.3048;
else $s="";
return $s;
}

Autre problème : chaque ligne NMEA ne donne pas toutes les informations, elles sont réparties sur plusieurs et moi j’ai besoin de l’altitude, le positionnement géographique plus la date et l’heure. Tout cela est réparti sur 2 lignes distinctes. Dans mon fichiers NMEA il me faut donc avoir autant de ligne de l’une que de l’autre, hors, le GPS peut avoir été arrêté brutalement, il peut y avoir un décalage. En plus vous le verrez, certaines fois, une donnée du fichier est totalement inexploitable (c’est pour ça que dans les fonctions, je vérifie que ce que l’on me donne est correct et sinon, je renvoie une chaîne vide. Mais si une valeur est inexploitable, c’est toute la ligne qui l’est et du coup la ligne complémentaire aussi.

Et voilà donc la fonction finale qui transforme un fichier NMEA en fichier KML (je n’ai pas du tout utilisé la bibliothèque XML de php et c’est volontaire !! Ceci permettra à d’éventuels lecteurs de transformer le code dans n’importe quel autre langage. Le but étant de partager le principe intellectuel et pas de frimer avec l’utilisation de bibliothèques non disponibles par défaut ou la création de classes ou encore avec des expressions régulières.

Enfin les CRLF (\r\n) sont là pour pouvoir facilement relire le code dans un navigateur web via l’affichage du source de la page.

function nmeafile_to_kmlfile($nmeafile,$kmlfile)
{
$result=new stdclass();
$result->CR="0";
$result->MSG="";
$DATA="";
$DATA_GPGGA=array(); 
$DATA_GPRMC=array(); 
if (file_exists($kmlfile)) @unlink($kmlfile);


 if (!file_exists($nmeafile))
{
$result->CR="-1";
$result->MSG="Absence de ".$nmeafile;
return $result;
}
$fh=fopen($nmeafile,"r");
if (!$fh) 
{
$result->CR="-1";
$result->MSG="Impossilbe d'ouvrir ".$nmeafile;
return $result;
}


$igpgga=0;
$igprmc=0;
$A_DATA_TIME="";
$A_DATA_LAT="";
$A_DATA_LONG="";
$A_DATA_ALT="";
$R_DATA_LAT="";
$R_DATA_LONG="";
$R_DATA_DATETIME="";
while (($data = fgetcsv($fh, 1000, ",")) !== FALSE) 
{
$num = count($data);
$ident=(isset($data[0])?$data[0]:"");
//retirer le 1er char ($)
if (substr($ident,0,1)=="$") $ident=substr($ident,1);
if (($ident=="GPGGA") and (count($data)>=11))
{
$A_DATA_TIME=$data[1]; //125102.00

if ($data[3]=="S") $A_DATA_LAT="-"; else $A_DATA_LAT="";
$A_DATA_LAT.=$data[2]; //4909.88037,N

if ($data[5]=="W") $A_DATA_LONG="-"; else $A_DATA_LONG="";
$A_DATA_LONG.=$data[4]; //00218.73914,E

$A_DATA_ALT=strtoupper($data[10]).$data[9]; //M48.0

/********************************************/
/*conversion*/
$A_DATA_LAT=nmeadeg_to_kmldeg($A_DATA_LAT);
$A_DATA_LONG=nmeadeg_to_kmldeg($A_DATA_LONG);
$A_DATA_ALT=nmeaalt_to_kmlalt($A_DATA_ALT);
/********************************************/
}
elseif (($ident=="GPRMC") and (count($data)>=10)) 
{
$R_DATA_TIME=$data[1]; //125102.00

if ($data[4]=="S") $R_DATA_LAT="-"; else $R_DATA_LAT="";
$R_DATA_LAT.=$data[3]; //4909.88037,N

if ($data[6]=="W") $R_DATA_LONG="-"; else $R_DATA_LONG="";
$R_DATA_LONG.=$data[5]; //00218.73914,E

$R_DATA_DATE=$data[9]; //030819

/********************************************/
/*conversion*/
$R_DATA_LAT=nmeadeg_to_kmldeg($R_DATA_LAT);
$R_DATA_LONG=nmeadeg_to_kmldeg($R_DATA_LONG);
$R_DATA_DATETIME=nmeadatetime_to_kmldatetime($R_DATA_DATE,$R_DATA_TIME);
/********************************************/
}
if (($A_DATA_TIME!="") and ($A_DATA_LAT!="") and ($A_DATA_LONG!="") and ($A_DATA_ALT!="") and ($R_DATA_LAT!="") and ($R_DATA_LONG!="") and ($R_DATA_DATETIME!=""))
{
if ($ident=="GPGGA") 
{
if (!isset($DATA_GPGGA[$igpgga])) $DATA_GPGGA[$igpgga]=new stdclass(); 
$DATA_GPGGA[$igpgga]->DATA_TIME=$A_DATA_TIME;
$DATA_GPGGA[$igpgga]->DATA_LAT=$A_DATA_LAT;
$DATA_GPGGA[$igpgga]->DATA_LONG=$A_DATA_LONG;
$DATA_GPGGA[$igpgga]->DATA_ALT=$A_DATA_ALT;
$igpgga++;
}
elseif ($ident=="GPRMC") 
{
if (!isset($DATA_GPRMC[$igprmc])) $DATA_GPRMC[$igprmc]=new stdclass(); 
$DATA_GPRMC[$igprmc]->DATA_DATETIME=$R_DATA_DATETIME;
$DATA_GPRMC[$igprmc]->DATA_LAT=$R_DATA_LAT;
$DATA_GPRMC[$igprmc]->DATA_LONG=$R_DATA_LONG;
$igprmc++;
$A_DATA_LAT="";
$A_DATA_LONG="";
$A_DATA_DATETIME="";
}
}
} 
fclose($fh);
$nrmc=count($DATA_GPRMC);
$nnmea=count($DATA_GPGGA);
if (($nrmc==0) or ($nnmea==0))
{
$result->CR="100";
$result->MSG="Pas de données exploitables";
return $result;
}
//il faut le même nombre de lignes entre les 2 types de phrases (au cas où)
$n=($nrmc<$nnmea?$nrmc:$nnmea);
while ($n<count($DATA_GPRMC)) unset($DATA_GPRMC[count($DATA_GPRMC)-1]);
while ($n<count($DATA_GPGGA)) unset($DATA_GPGGA[count($DATA_GPGGA)-1]);
//Création XML
$DATA="";
$DATA.="<?xml version='1.0' encoding='UTF-8'?>\r\n";
$DATA.="<kml xmlns='http://www.opengis.net/kml/2.2' xmlns:gx='http://www.google.com/kml/ext/2.2' xmlns:kml='http://www.opengis.net/kml/2.2' xmlns:atom='http://www.w3.org/2005/Atom'>\r\n";
$DATA.="<Document>\r\n";
$DATA.="<name>GPS device</name>\r\n";
$DATA.="<snippet>Created ".date("d/m/y H:i:s")."</snippet>\r\n";
$DATA.="<LookAt>\r\n";
$DATA.="<gx:TimeSpan>\r\n";
$DATA.="<begin>".$DATA_GPRMC[0]->DATA_DATETIME."</begin>\r\n";
$DATA.="<end>".$DATA_GPRMC[$n-1]->DATA_DATETIME."</end>\r\n";
$DATA.="</gx:TimeSpan>\r\n";
$DATA.="<longitude>".$DATA_GPRMC[0]->DATA_LONG."</longitude>\r\n";
$DATA.="<latitude>".$DATA_GPRMC[0]->DATA_LAT."</latitude>\r\n";
$DATA.="<altitude>0</altitude>\r\n";
$DATA.="<heading>0</heading>\r\n";
$DATA.="<tilt>0</tilt>\r\n";
$DATA.="<range>10000</range>\r\n";
$DATA.="</LookAt>\r\n";
$DATA.="<Style id='multiTrack_h'>\r\n";
$DATA.="<IconStyle>\r\n";
$DATA.="<scale>1.2</scale>\r\n";
$DATA.="<Icon>\r\n";
$DATA.="<href>http://earth.google.com/images/kml-icons/track-directional/track-0.png</href>\r\n";
$DATA.="</Icon>\r\n";
$DATA.="</IconStyle>\r\n";
$DATA.="<LineStyle>\r\n";
$DATA.="<color>99ffac59</color>\r\n";
$DATA.="<width>8</width>\r\n";
$DATA.="</LineStyle>\r\n";
$DATA.="</Style>\r\n";
$DATA.="<StyleMap id='multiTrack'>\r\n";
$DATA.="<Pair>\r\n";
$DATA.="<key>normal</key>\r\n";
$DATA.="<styleUrl>#multiTrack_n</styleUrl>\r\n";
$DATA.="</Pair>\r\n";
$DATA.="<Pair>\r\n";
$DATA.="<key>highlight</key>\r\n";
$DATA.="<styleUrl>#multiTrack_h</styleUrl>\r\n";
$DATA.="</Pair>\r\n";
$DATA.="</StyleMap>\r\n";
$DATA.="<Style id='multiTrack_n'>\r\n";
$DATA.="<IconStyle>\r\n";
$DATA.="<Icon>\r\n";
$DATA.="<href>http://earth.google.com/images/kml-icons/track-directional/track-0.png</href>\r\n";
$DATA.="</Icon>\r\n";
$DATA.="</IconStyle>\r\n";
$DATA.="<LineStyle>\r\n";
$DATA.="<color>99ffac59</color>\r\n";
$DATA.="<width>6</width>\r\n";
$DATA.="</LineStyle>\r\n";
$DATA.="</Style>\r\n";
$DATA.="<Folder>\r\n";
$DATA.="<name>Tracks</name>\r\n";
$DATA.="<Placemark>\r\n";
$DATA.="<styleUrl>#multiTrack</styleUrl>\r\n";
$DATA.="<gx:Track>\r\n";
$DATA.="<altitudeMode>absolute</altitudeMode>\r\n";
foreach($DATA_GPRMC as $row)
{
$DATA.="<when>".$row->DATA_DATETIME."</when>\r\n";
}
for ($i=0;$i<count($DATA_GPRMC);$i++)
{
$DATA.="<gx:coord>".$DATA_GPRMC[$i]->DATA_LONG." ".$DATA_GPRMC[$i]->DATA_LAT." ".$DATA_GPGGA[$i]->DATA_ALT."</gx:coord>\r\n";
} 
$DATA.="</gx:Track>\r\n";
$DATA.="</Placemark>\r\n";
$DATA.="</Folder>\r\n";
$DATA.="</Document>\r\n";
$DATA.="</kml>\r\n";

$fh=fopen($kmlfile,"w+");
if (!$fh)
{
$result->CR="-1";
$result->MSG="Impossible de créer ".$kmlfile;
return $result;
} 
fwrite($fh,$DATA,strlen($DATA));
fclose($fh);
return $result;
}









 

 

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.