30 déc
Géocodage en php avec les API Google et Yahoo Maps
Après plusieurs mois sans avoir ajouté un seul article sur mon blog, je reviens avec une fonction complète qui facilitera bien la vie à certains intéressé par les coordonnées GPS et surtout le moyen de les générer à plus ou moins grande échelle.
Je suis partis du principe que ma fonction devrait être utilisée dans n’importe qu’elle circonstance ou en tout cas dans une grande partie. Elle se devait d’être légère et rapide si on lui demande, ou un peu plus lente mais plus détaillée si l’on souhaite avoir une liste de choix proposés.
Le tout sans trop ralentir le chargement de la page, sans générer d’erreur de type warning ou notice si on tente de mettre une adresse foireuse etc. Bref, un petit condensé de 6 heures de travail est présent dans ce post ; détaillant le cheminement du développement de l’idée de départ à son aboutissement final.
Vous ne savez pas ce que sont les coordonnées GPS? Petite explication très bien faite en vidéo :
Cette vidéo explique simplement le principe des coordonnées GPS en degrés. Pour les utilisations en base de données on ne travaille quasiment qu’avec des coordonnées décimales (en tout cas, dans mon cas). Le principe est le même, à l’exception qu’on a juste un nombre décimal pour la latitude et un nombre décimal pour la longitude.
La fonction que je vous détaille ci-dessous retournera donc des coordonnées décimales. Si ensuite on veux les convertir en degrés, il faudra soit créer une autre fonction pour ça, soit modifier celle-ci.
Bref, trêve de bla bla, place au code ! ![]()
Donc, ci-dessous, le code copié / collé brut de la fonction, les commentaires devraient suffir à bien expliquer son fonctionnement :
## FONCTION findGps($google_key, $yahoo_key, $adresse [, bool $plusieurs=false] [, int $reload=0] [, bool $yahoo=false)
# ======================================================================================================================
# Developpee par Denis Métral-Deschamps (Denis Dee Jay) - http://www.denisdeejay.com/
# Date de création : 07/12/2009
#
#
#
# RETOURNE
# --------
# Un array a 2 ou 3 dimensions en fonction du resultat (2 si echec, 3 si reussite)
# Array('status' => 200, INT (200=OK ; 602=Rien ; etc.. (voir doc Google en bas de page))
# 'results' => Array(0 => INT (peut etre duplique X fois pour X resultats)
# Array('precision' => 4, INT ou STRING (Code de precision (Google), String de précision (Yahoo))
# 'adresse' => 'Grenoble France', STRING (Adresse reformattee)
# 'latitude' => 45.193987, FLOAT (latitude au format decimal)
# 'longitude' => 5.732003))); FLOAT (longitude au format decimal)
#
#
#
# PARAMETRES
# ----------
# $key_gg = (string) Clé de l'API Google Maps
# $key_yy = (string) Clé de l'API Yahoo Maps
# $adresse = (string) String contenant l'adresse a localiser
# OU
# (array) Array contenant d'une a 5 cles : 'adresse', 'adresse2', 'code_postal', 'ville' et 'pays'
# $plusieurs = (bool) (default : false). Optionnel. Détermine si l'on peut retourner un (false) ou plusieurs resultats (true)
# $relaod = (int) (default : 0). Optionnel. Est un compteur utilisé par la fonction en cas de récursivité.
# $yahoo = (bool) (default : false). Optionnel. Est utilisé par la fonction en cas de récurisivité. Si $yahoo=true la fonction utilisera l'API Yahoo
#
#
#
# PRINCIPE DE FONCTIONNEMENT
# --------------------------
# La fonction génère des coordonnées GPS pour une adresse grace à l'API Google Maps et à l'API Yahoo Maps.
# -
# Si un seul résultat est demandé, la fonction privilégiera le format de retour CSV de l'API Google Maps (plus léger et plus rapide), sinon elle se servira du format XML.
# Le format de retour PHP est utilisé pour l'API Yahoo Maps (plus léger, donc plus rapide que ses homologues JSON et XML).
# -
# Cette fonction a été développée afin de pouvoir être utilisé dans une boucle. En premmier lieu elle tente avec Google Maps. Google ne tolère pas que l'on
# fasse trop de requêtes dans un court laps de temps. En fonction de la réponse (vide ou status 620) la fonction marque une pause d'une seconde
# puis se relance récursivement en incrémentant le compteur $reload. Au dela de 5 échecs (vide ou status 620), la fonction tente avec Yahoo Maps une dernière fois.
# -
# Cette fonction a été développée afin de pouvoir être utilisé dans un certain nombre de cas de figure. La variable $adresse peut recevoir soit
# un array formatté (voir ci-dessus) soit une string. Permettre plusieurs résultats en sortie donne la possibilité de créer une liste de choix comme on le ferait
# avec un géocodeur en JS (voir : http://www.vousfinancer.com/agences/) mais dans un langage serveur.
# Limiter les résultats à un seul enregistrement quand à lui sera beaucoup plus performant dans le cas d'une génération massive de coordonnées GPS provenant
# par exemple d'une base de données d'adresses.
# -
# Attention toutefois à une chose : Google limite à 15000 requetes par IP pour 24h. Yahoo limite à 5000 requetes par clés API.
# Pour info : Un géocodeur JS a lui aussi ses limitations mais l'IP utilisée est celle du poste client et non pas celle du serveur. (quasi illimité donc)
#
#
#
# EXEMPLES D'UTILISATION
# ----------------------
# $my_array = findGps($my_key_gg, $my_key_yy, $my_adresse); // utilisation classique et rapide. Un seul resultat retourné
# $my_array = findGps($my_key_gg, $my_key_yy, $my_adresse, true); // utilisation avancée. Permet de retourner plusieurs resultats si disponibles
# $my_array = findGps($my_key_gg, $my_key_yy, $my_adresse, true, 0, true); // utilisation spécifique. Force l'utilisation de l'API Yahoo
#
#
# MERCI de laisser ce commentaire dans vos codes. Ca ne vous coute rien car personne ne le voit mais au moins on sait qui a travaillé sur ça
# ==============================================================================================================================================
function findGps($key_gg, $key_yy, $adresse, $plusieurs=false, $reload=0, $yahoo=false){
# VARIABLES DE CONFIGURATION
$api_ggmaps = 'http://maps.google.fr/maps/geo?'; // maps FR plus precise pour les localités FR
$api_yahoo = 'http://local.yahooapis.com/MapsService/V1/geocode?';
$pause = 1000000; // soit 1000 millisecondes soit 1 sec
$max_reload = 5; // 5 reload max
$methode = 'csv'; // ou 'xml', ou 'json' (utilise pour API Google)
$str_query = '';
# UNE SOUS FONCTION FORMATTANT PROPREMENT L'ADRESSE
// notre fonction findGps est recursive, on evite de re-declarer 2 fois la meme sous fonction
if(!function_exists('formatQuery')){
function formatQuery($adresse){
$str = '';
// compliation en string si c'est un array
if(is_array($adresse)){
$str.= (isset($adresse['adresse'])) ? $adresse['adresse'] : '';
$str.= (isset($adresse['adresse2'])) ? ' '.$adresse['adresse2'] : '';
$str.= (isset($adresse['code_postal'])) ? ', '.$adresse['code_postal'] : '';
$str.= (isset($adresse['ville'])) ? ' '.$adresse['ville'] : '';
$str.= (isset($adresse['pays']) && !empty($adresse['pays'])) ? ', '.$adresse['pays'] : '';
// sinon, c'est une string
} else { $str = $adresse; }
// nettoyage de la requete
$str = trim(strip_tags($str));
$str = str_replace("\n", ' ', $str);
$str = str_replace("\r", ' ', $str);
$str = str_replace("\t", ' ', $str);
$str = str_replace(' ,', ',', $str);
$str = preg_replace('/\s{2,}/', ' ', $str); // des que plus de 2 espace, remplace par un seul
$str = stripslashes($str);
// on verifie l'encodage
if(mb_detect_encoding($str,"UTF-8, ISO-8859-1, GBK") != 'UTF-8'){
$str = utf8_encode($str);
}
// return
return $str;
}
}
# UNE SOUS FONCTION PERMETTANT D'APPELLER L'API YAHOO SI BESOIN
// notre fonction findGps est recursive, on evite de re-declarer 2 fois la meme sous fonction
if(!function_exists('gpsYahoo')){
function gpsYahoo($url_yy, $key_yy, $adresse, $plusieurs=false){
$url_query = $url_yy.'appid='.$key_yy.'&location='.urlencode($adresse).'&output=php'; // 'output=xml' ou 'output=json' possible aussi
$geo_get = @file_get_contents($url_query); // si bug, file_get_contents retourne un warning, on le bloque avec le @
$geocode = ($geo_get) ? unserialize($geo_get) : false;
// si un seul resultat, yahoo ne retounera pas de sous array mais un seul contenant 8 cles, on teste donc la presence du dernier
$only_1 = (count($geocode['ResultSet']['Result']) == 8 && !isset($geocode['ResultSet']['Result'][7])) ? true : false;
$tbl_gps = array('status' => ($geocode) ? 200 : 602, 'results' => array());
if(!$geocode){ return $tbl_gps; }
$nbr = ($plusieurs && !$only_1) ? count($geocode['ResultSet']['Result']) : 1;
for($i=0; $i<$nbr; $i++){
$lieu = ($only_1) ? $geocode['ResultSet']['Result'] : $geocode['ResultSet']['Result'][$i];
$tbl_gps['results'][$i] = array();
$tbl_gps['results'][$i]['precision'] = (string) $lieu['precision'];
$tbl_gps['results'][$i]['adresse'] = (string) utf8_decode(trim($lieu['Address'].' '.$lieu['City'].' '.$lieu['State'].' '.$lieu['Zip'].' '.$lieu['Country']));
$tbl_gps['results'][$i]['latitude'] = (float) $lieu['Latitude'];
$tbl_gps['results'][$i]['longitude'] = (float) $lieu['Longitude'];
}
return $tbl_gps;
}
}
# FORMATTAGE DE L'ADRESSE
$str_query = formatQuery($adresse);
# SI YAHOO DEMANDE
if($yahoo){ return gpsYahoo($api_yahoo, $key_yy, $str_query, $plusieurs); }
# SI UN SEUL RESULTAT DEMANDE, ON PASSE PAR LE CSV QUI EST PLUS LEGER
if(!$plusieurs){
$url_query = $api_ggmaps.'q='.urlencode($str_query).'&output='.$methode.'&oe=utf8&key='.$key_gg;
$geocode = file_get_contents($url_query);
$tbl_gps = ($geocode) ? explode(',' , $geocode) : false;
$tbl_ret = array('status' => (int) $tbl_gps[0],
'results' => array());
// gestion du code 620 (trop de requetes dans un petit laps de temps)
if(!$tbl_ret['status'] || $tbl_ret['status'] == 620 && $reload < $max_reload){ usleep($pause); return findGps($key_gg, $key_yy, $str_query, $plusieurs, ($reload+1)); }
if(!$tbl_ret['status'] || $tbl_ret['status'] == 620 && $reload >=$max_reload){ return findGps($key_gg, $key_yy, $str_query, $plusieurs, 0, true); } // passe sur yahoo
if($tbl_ret['status'] == 602){ return findGps($key_gg, $key_yy, $str_query, $plusieurs, 0, true); } // passe sur yahoo
$tbl_ret['results'][0] = array('precision' => (int) $tbl_gps[1],
'adresse' => (string) $str_query,
'latitude' => (float) $tbl_gps[2],
'longitude' => (float) $tbl_gps[3]);
return $tbl_ret;
}
# SI PLUSIEURS RESULTATS DEMANDE, ON PASSE PAR LE XML QUI EST PLUS COMPLET
if($plusieurs){
$methode = 'xml';
$url_query = $api_ggmaps.'q='.urlencode($str_query).'&output='.$methode.'&oe=utf8&key='.$key_gg;
$xml = simplexml_load_file($url_query);
// si trop de requetes, le flux xml peut ne pas etre disponible tout simplement
if(!$xml && $reload < $max_reload) { usleep($pause); return findGps($key_gg, $key_yy, $str_query, $plusieurs, ($reload+1)); }
if(!$xml && $reload >=$max_reload) { return findGps($key_gg, $key_yy, $str_query, $plusieurs, 0, true); } // passe sur yahoo
$tbl_gps = array('status' => (int) $xml->Response->Status->code,
'results' => array());
// gestion du code 620 (trop de requetes dans un petit laps de temps)
if(!$tbl_gps['status'] || $tbl_gps['status'] == 620 && $reload < $max_reload){ usleep($pause); return findGps($key_gg, $key_yy, $str_query, $plusieurs, ($reload+1)); }
if(!$tbl_gps['status'] || $tbl_gps['status'] == 620 && $reload >=$max_reload){ return findGps($key_gg, $key_yy, $str_query, $plusieurs, 0, true); } // passe sur yahoo
if($tbl_gps['status'] == 602){ return findGps($key_gg, $key_yy, $str_query, $plusieurs, 0, true); } // passe sur yahoo
for($i=0; $i<count($xml->Response->Placemark); $i++){
$gps = explode(',' , $xml->Response->Placemark[$i]->Point->coordinates);
$tbl_gps['results'][$i] = array();
// note : typer la variable permet d'eviter qu'un objet nous soit retourne
$tbl_gps['results'][$i]['precision'] = (int) $xml->Response->Placemark[$i]->AddressDetails['Accuracy'];
$tbl_gps['results'][$i]['adresse'] = (string) $xml->Response->Placemark[$i]->address;
$tbl_gps['results'][$i]['latitude'] = (float) $gps[1];
$tbl_gps['results'][$i]['longitude'] = (float) $gps[0];
}
return $tbl_gps;
}
}
/*
codes erreures de Google
------------------------
200 = G_GEO_SUCCESS -- (Since 2.55) -- No errors occurred; the address was successfully parsed and its geocode has been returned.
400 = G_GEO_BAD_REQUEST -- (Since 2.81) -- A directions request could not be successfully parsed. For example, the request may have been rejected if it contained more than the maximum number of waypoints allowed.
500 = G_GEO_SERVER_ERROR -- (Since 2.55) -- A geocoding or directions request could not be successfully processed, yet the exact reason for the failure is not known.
601 = G_GEO_MISSING_QUERY -- (Since 2.81) -- The HTTP q parameter was either missing or had no value. For geocoding requests, this means that an empty address was specified as input. For directions requests, this means that no query was specified in the input.
601 = G_GEO_MISSING_ADDRESS -- (Since 2.55) -- Synonym for G_GEO_MISSING_QUERY.
602 = G_GEO_UNKNOWN_ADDRESS -- (Since 2.55) -- No corresponding geographic location could be found for the specified address. This may be due to the fact that the address is relatively new, or it may be incorrect.
603 = G_GEO_UNAVAILABLE_ADDRESS -- (Since 2.55) -- The geocode for the given address or the route for the given directions query cannot be returned due to legal or contractual reasons.
604 = G_GEO_UNKNOWN_DIRECTIONS -- (Since 2.81) -- The GDirections object could not compute directions between the points mentioned in the query. This is usually because there is no route available between the two points, or because we do not have data for routing in that region.
605 = G_GEO_UNKNOWN_ADDRESS -- (Since 2.55) -- No corresponding geographic location could be found for the specified address. This may be due to the fact that the address is relatively new, or it may be incorrect.
610 = G_GEO_BAD_KEY -- (Since 2.55) -- The given key is either invalid or does not match the domain for which it was given.
620 = G_GEO_TOO_MANY_QUERIES -- (Since 2.55) -- The given key has gone over the requests limit in the 24 hour period or has submitted too many requests in too short a period of time. If you're sending multiple requests in parallel or in a tight loop, use a timer or pause in your code to make sure you don't send the requests too quickly.
*/
Vu que le code est très large, je vous conseille de le voir en plein écran après avoir cliqué sur la même icone que celle-ci : 


Posté par Denis Dee Jay le 30 décembre 2009 à 16 h 43 min
Bon, j’ai fais au mieux, mais on peut dire que c’est tout sauf simple de poster un code aussi long (et large surtout) sans que ça fasse planter la page ^^
Posté par philgood le 4 mars 2010 à 18 h 55 min
bonjour Denis
excellent ton billet. Marche très bien avec mon php5 installé sur IIS7 vista home premium
Pas trop de connaissances en php mais ton code est bien documenté et avec un peu de recherche on trouve.
Si ca peut servir à d’autres, voila ce que j’ai ajouté à la fin de ton code (et ne pas oublier les balises php au debut :
// Programme principal $my_key_gg = "AB.................a0Atn8tlzrw"; $my_key_yy = ''; $my_adresse = "14 rue marcel xxxxx .......... FRANCE"; $my_array = findGps($my_key_gg, $my_key_yy, $my_adresse); foreach($my_array as $cle=>$valeur) { if (count($my_array) == 2) { if(is_array($valeur)) { foreach($valeur as $cl=>$va) { if(is_array($va)) { foreach($va as $c=>$v) { echo $c.' : '.$v.''; } }else {echo $cl.' : '.$va.'';} } }else {echo $cle.' : '.$valeur.'';} }else {echo "Echec de Géocodage." ." Status = " .$my_array['status'] .'';} } ?>ou plus simplement pour l’affichage :
echo »;
print_r($my_array);
echo »;
J’ai une toute petite erreur HTTP 500.0 avec ce bout de code
// on verifie l'encodage if(mb_detect_encoding($str,"UTF-8, ISO-8859-1, GBK") != 'UTF-8'){ $str = utf8_encode($str); }… à suivre
phil
Posté par Denis Dee Jay le 4 mars 2010 à 19 h 34 min
Bonsoir Phil,
Merci pour ton message, ça fait toujours plaisir de voir que quelqu’un apprécie son travail
Pour ton erreur 500 sur la fonction mb_detect_encoding, j’avais encore jamais vu ça, bizarre …
Il y a d’autres façons de détecter l’encodage, voir dans les commentaires de php.net pour ça : http://php.net/manual/fr/function.mb-detect-encoding.php
Sinon, moins propre mais fonctionnel faire un utf8_encode(utf8_decode($str)) en virant la condition.
C’est drôle, je me suis remis au géocoding aujourd’hui et j’ai ajouté de nouvelles fonctionnalités à mes scripts (la plupart feront l’objet d’autres billets, sauf une qui sera ajouté à celui là).
@+
Posté par philgood le 4 mars 2010 à 20 h 03 min
bonsoir Denis
j’ai viré la condition et utf8_encode fonctionne bien; c’est mb_detect_encoding qui me provoque l’erreur (pour ma config pc)
Merci de ta rapide réponse
si tu peux reformatter un peu ma réponse et encadrer correctement le print_r($my_array);
par des echo pre et /pre; qui vont bien car l’affichage ne se fait pas correctement une fois le post validé!!.
++ et impatient de voir la suite de tes billets ^^
Phil
Posté par philgood le 4 mars 2010 à 21 h 01 min
hi Denis
Encore moi ^^ … j’aime bien ton prog …
J’ai essayé le mode avancé (plusieurs résultats)
Ca fonctionne bien mais les résultats latitude et longitude sont permutés.
nb : j’ai mis une chaine vide pour la key API yahoo, mais apparemment ca fonctionne bien sans.
merci à toi
Posté par philgood le 4 mars 2010 à 21 h 10 min
‘lut Denis
en faisant la correction suivante, je n’ai plus la permutation des latitude et longitude mais comme j’y suis allé à l’instinct merci de vérifier mes dire :
$tbl_gps['results'][$i]['latitude'] = (float) $gps[1];
$tbl_gps['results'][$i]['longitude'] = (float) $gps[0];
++
Posté par Denis Dee Jay le 4 mars 2010 à 22 h 55 min
Re Phil
Donc, dans l’ordre :
- pour le mb_detect_encoding, je pense que ça vient du fait que tu bosses en local. Les pc Fr sont configurés pour le latin (iso-8859-1 et iso-8859-15), donc possible que ça foire pour un test sur de l’utf-8.
- l’ajout d’un pre dans ton 1er commentaire -> ok
- pour la clé de l’API Yahoo : ça c’est surement du au fait que tu sois en local (donc http://localhost/ surement) car si tu testes en direct dans ton navigateur, tu te prend un message d’erreur :
http://local.yahooapis.com/MapsService/V1/geocode?appid=&location=Grenoble
alors qu’en renseignant la clé ça passe.
- Effectivement, c’était inversé. Ce qui est hallucinant, c’est que c’est un copié / collé brut d’un code présent sur l’un de mes serveurs et que sur ce serveur, le code est dans le bon sens ! J’avais du virer des trucs puis les remettre vite fait sans me relire, merci de m’avoir prévenu
Si tu veux faire des tests, je te conseille de faire comme j’avais fais à l’époque (ce script n’est plus tout jeune, je l’ai noté au 07/12/2009 dans le code mais sa toute première mouture rre-crée quasi intégralement en décembre, mais je savais déjà comment se comportait les api yahoo et google à l’époque ^^).
je faisais des essais en direct dans le navigateur :
http://maps.google.fr/maps/geo?q=vinay&output=xml
http://maps.google.fr/maps/geo?q=vinay&output=csv
C’est comme ça que je me suis rendu compte que le csv était beaucoup plus utile quand on fait défiler 10 000 entrées d’une base de données ^^
Pour mes futurs billets, ils sont tirés de mes travaux sur : http://www.vousfinancer.com/agences/
A savoir, entre autre :
- géocode en ajax via des flux en json
- gestion de regroupement de marqueurs (cluster markers ou clustering pour google maps)
- comment poster un objet json à une map et l’afficher (plus léger qu’une génération de js)
- dessins / édition de polygones en javascript avec l’enregistrement en bdd en tant que coordonnées spatiales (ce qui permet ensuite, si on cherche à trouver dans quel polygone les coordonnées gps que l’on a a disposition vont, on le fait via une simple requete sql, plus besoin de passer par google ou yahoo)
- géolocalisation de base au km à la ronde en sql
- etc.
Bref, j’ai plein de codes déjà prêts depuis des mois à poster ici, c’est juste qu’ils sont presque autant commentés que ce billet mais j’ai pas encore rédigé la partie expliquant la logique du code et le cheminement des process.
Voilou
@+
Posté par Denis Dee Jay le 4 mars 2010 à 23 h 18 min
Tiens, j’avais oublié de te préciser ça :
Sur ces lignes :
$tbl_gps['results'][$i]['precision'] = (int) $xml->Response->Placemark[$i]->AddressDetails['Accuracy'];
Au cas où tu ai a faire des modifs, laisse le typage des variable, et plus particulièrement sur celle là. Car étant donné que c’est un attribut de flux xml, si tu ne la type pas, la fonction l’interprètera comme un objet et non pas comme un integer ! Je me souviens avoir mis un petit moment à trouver cette astuce toute bête ; de plus, le typage de variable est plus propre et le code s’execute plus rapidement.
Autre truc : tous mes codes sont testés directement sur des serveurs linux (centOs Linux Plesk ou gentoo release 2 ou debian en fonction du projet), ça peut expliquer qu’on ait des différences de résultats puisque tu es sur windows.
Enfin, en discutant avec toi je me suis rendu compte que le thème du blog (WPburn blue) affichait la date du post a la place de la date du commentaire au dessus de nos comm’ ; ça m’a permis de m’en rendre compte et donc de rectifier cette erreur ; merci
++
Posté par philgood le 5 mars 2010 à 9 h 07 min
Bonjour Denis
Tu sais transmettre l’envie de Savoir. (h)
Pédagogue et compétent, tu es mon bon Prof en Géoloc/PHP
merci pour toutes ces précisions de qualité.
Oui je teste les scripts en localhost; je vais essayer celà avec mon ip dyndns toute fraiche sur mon serveur iis7 vista (faut bien débuter …
).
A bientôt de lire tes sujets et comme ils m’ont l’air bien technique, ne néglige pas la partie expliquant leur logique :p
bonne journée
Phil