10/ 10/2009

Recherche "à proximité"

Théorie

Le point central (center)et les données à rechercher doivent contenir leur latitude et leur longitude, exprimées en degré décimal. La distance de recherche (radius) est exprimée en kilomètres.

La recherche s'effectue en 3 phases:

Calcul des points extrêmes

Les points extrêmes permettent de définir un carré dont le centre est le point initial et dont la largeur est le double de la distance de recherche. Pour cela, il faut convertir cette distance en degré décimal.
Sachant qu'un mile nautique représente une minute d'angle terrestre et correspond (approximativement) à 1852 mètres, on peut donc définir qu'un kilomètre est égal à 1/1,852 minute d'angle, soit 1(1,852*60) degré décimal.
La conversion du rayon donne donc: r = rayon / (1.852*60)
Le point central est représenté par lat et lng.
Les points extrêmes sont donc:

  • latMin = lat - r
  • latMax = lat + r
  • lngMin = lng - r
  • lngMax = lng + r

Recherche maximale des points

La table "lieux" est en fait la table des villes définie dans le billet Géolocalisation par IP.
La requête sera:

SELECT *
FROM lieux
WHERE (
   lat<=latMax
   AND lat>=latMin
   AND lng<=lngMax
   AND lng>=lngMin
)

Ceci va donc nous ramener tous les points contenus dans notre carré.

Affinement au rayon

Pour trouver les points contenus dans le rayon, il faut éliminer calculer la distance exacte entre le point central et chacun des points retournés, la formule est: ((lat - latx)² + (lng -lngx)²)^0.5
Cette valeur étant en degré décimal, il nous suffit ensuite de ne pas garder les points dont la distance est supérieure au rayon recherché.

En pratique

$center est un tableau à deux éléments, lat et lng.
$radius est la distance en kilomètres.

<?php
/**
 * Retourne les points à proximité
 * Le point central est un tableau associatif de deux données:
 * lat => latitude
 * lng => longitude
 *@param array $center Point central
 *@param radius Rayon de recherche en kilomètres
 *@return array Tableau des points compris dans le rayon
 */
function searchProxy($center, $radius=10) {
 
	// Retour vide si $center n'est pas un tableau conforme
	if (!is_array($center) || empty($center['lat']) || empty($center['lng'])) return;
 
	// Retour vide si le rayon n'est pas valide
	if (intval($radius) == 0) return;
 
	// Conversion du rayon en degré décimal
	$radius = $radius / (60 * 1.852);
 
	// Recherche du carré maximal
	$query = 	"SELECT *
			FROM lieux
			WHERE (
				lat<=".($center['lat'] + $radius)."
				AND lat>=".($center['lat'] - $radius)."
				AND lng<=".($center['lng'] + $radius)."
				AND lng >=".($center['lng'] - $radius)."
			)";
	$res = mysql_query( $query );
	if (mysql_num_rows($res)==0) return;
	$points = array();
 
	// Elimination des points hors du cercle
	while ($row = mysql_fetch_assoc($res)) {
		$dist = sqrt(pow(($center['lat'] - $row['lat']), 2) + pow(($center['lng'] - $row['lng']),2));
		if ($dist>$radius) continue;
		$points[] = $row;
	}
	return $points;
}
?>

Nota

Il aurait bien entendu été possible de ne tout faire qu'avec une seule requête, mais les calculs mathématiques en SQL (et particulièrement sur MySQL) prennent énormément de ressources et de temps. Voici tout de même la requête "globale":

SELECT *
FROM lieux
WHERE (
   SQRT(POW((lat - ".$center['lat']."), 2) + POW((lng - ".$center['lng']."),2)) < ".$radius."
)

Géolocalisation par IP

Pré-requis

La meilleure solution à ce jour me semble être l'utilisation de la base GeoLite city. Elle comporte 2 fichiers csv qu'il faut importer dans les tables suivantes:

Table de géolocalisation

Champ               Index                  Commentaire
startIpNum          Index                  Première IP numérique
endIpNum            Index                  Dernière IP numérique
locId                                      ID du lieu

StartIpNum et endIpNum sont les adresses IP converties en valeur numérique. La méthode de conversion est, si on considère une IP sous la forme AAA.BBB.CCC.DDD: (AAA * 256^3) + (BBB * 256^2) + (CCC * 256) + DDD

Table des villes

Champ                Index                  Commentaire
locId                Primaire               ID du lieu
country              Index                  Pays
region                                      Région administrative
city                                        Ville
postalCode                                  Code postal
latitude             Index                  Latitude
longitude            Index                  Longitude
metroCode
areaCode

Les bases sont assez grosses, donc si vous savez d'avance quels pays vous ciblez, n'hésitez pas à les réduire à ces pays.

Géolocalisation:

Pour retrouver la localisation d'une personne par son IP, il suffit d'une simple requête SQL qui utilisera l'IP transformée:

<?php
/**
 * Fonction retournant les informations géographiques
 *@param string $ip IP du visiteur
 *@return array Tableau contenant la ville et ses coordonnées GPS
 */
function geoloc($ip) {
	// Conversion de l'IP littérale en IP numérique
	$tmp = explode('.', $ip);
	$ipnum = ($tmp[0]*pow(256,3)) + ($tmp[1]*pow(256,2)) + ($tmp[2]*256) + $tmp[3];
	// Requête sur les deux tables pour retourner les informations
	$query = "SELECT c.city, c.latitude, c.longitude
	FROM geo_ip i
	JOIN geo_city c ON (c.locId=i.locID)
	WHERE ".$ipnum." BETWEEN startIpNum AND endIpNum
	LIMIT 1 OFFSET 0";
	$res = mysql_query($query);
	// Retour des informations (ou d'un tableau vide
	if (mysql_num_rows($res) == 0) return;
	return mysql_fetch_assoc($res);
}
// Récupération de l'IP du visiteur
$ip = $_SERVER['REMOTE_ADDR'];
$user = geoloc($ip);
?>

Comme vous pouvez le constater, le système est très simple à mettre en place et donne des résultats viables dans 99% des cas.