Avant de se lancer dans le développement, je vais essayer de vous expliquer le processus mis en oeuvre afin de clusteriser des éléments stockés en base de données.
Pour clusteriser les données, nous allons découper la carte visible à l'écran (plus un petit peu sur les côtés) en un certain nombre de 'petites' cartes - la carte centrale correspond à la partie visible (le navigateur de l'internaute n'affichera que cette partie centrale).
Le but étant de charger les marqueurs qui sont proches des bords de la carte, afin de fluidifier l'application.
Le but est de calculer pour chaque 'petite' carte le nombre de marqueurs qu'elle comporte.
Il est nécessaire de définir un nombre à partir duquel nous afficherons un marqueur qui invitera à zoomer (et indiquera le nombre d'éléments contenu dans la 'petite' carte). Si il y a moins de marqueur que ce nombre prédéfini, alors nous pourrons afficher l'ensemble des marqueurs de ce 'petit' bout de carte.
A chaque zoom ou chaque déplacement - supérieur à la distance du bord de la carte centrale au bord de l'extension, un appel Ajax demandera au serveur de recalculer les nouveaux éléments.
Le calcul des éléments sur les bords de la carte permet de pré-charger les POI et d'anticiper le déplacement de l'internaute afin de fluidifier l'application.
A chaque événement, il faudra donc envoyer au serveur les coordonnées des bords de la carte afin que celui-ci puisse calculer les POI et les marqueurs et puisse renvoyer le XML correspondant.
Tout d'abord il faut créer une fonction de création de marqueurs. Celle-ci différencie les marqueurs 'simples' des marqueurs 'cluster' avec une affichage différent et un événement spécifique à chacun.
L'API Google Maps permet d'utiliser la méthode XmlHttpRequest pour faire des appels à un serveur - il s'agit de la méthode GDownloadUrl déjà utilisée dans le tutoriel n°10.
Il faut commencer par supprimer les overlays présents sur la carte :
map.clearOverlays();
Puis récupérer les bords de la carte et le niveau de zoom que nous allons passer en paramètre lors de l'appel au serveur :
var urlstr = "./function/getObj.php?maxY="+maxY+"&minY="+minY+"&maxX="+maxX+"&minX="+minX+"&zoomLevel="+zoomLevel;
Enfin la méthode GDownloadUrl qui récupère le XML construit côté serveur en fonction des paramètres et qui appelle la fonction de création de marqueurs ci-dessus.
GDownloadUrl(urlstr,function(data,responseCode){if(responseCode==200){varxmlDoc=GXml.parse(data);varmarkers=xmlDoc.documentElement.getElementsByTagName("marker");varicon=newGIcon();icon.iconSize=newGSize(24.0,38.0);icon.shadowSize=newGSize(44.0,38.0);icon.iconAnchor=newGPoint(12.0,38.0);icon.infoWindowAnchor=newGPoint(12.0,19.0);for(i=0;i<markers.length;i++){varlatMarker=parseFloat(markers[i].getAttribute("lat_object"));varlngMarker=parseFloat(markers[i].getAttribute("lng_object"));if(markers[i].getAttribute("cluster")==0){vartitle=markers[i].getAttribute("title_object");createMarker(latMarker,lngMarker,title,icon,false);}else{vartitle=markers[i].getAttribute("nb_marker")+' items - zoom to view (click on marker)';createMarker(latMarker,lngMarker,title,icon,true);}}}});
Initialisation de la carte et création des événements#
Nous remarquerons qu'après la déclaration des options, nous appelons la méthode getMarker() afin d'afficher les marqueurs pour le niveau de zoom et l'extend par défaut.
Le but de ce tutoriel étant la clusterisation, il est nécessaire de déclarer un événement qui appelle la fonction getMarker() chaque fois que l'utilisateur modifie le zoom de la carte :
Il faut ensuite définir une méthode pour faire une requête au serveur chaque fois que l'utilisateur se déplace sur la carte en dépassant les limites de la 'grande' carte (cf. Comment faire ?).
Pour cela, il faut voir à chaque fin de déplacement (moveend) si la distance entre le nouveau centre et l'ancien centre et supérieure au pourcentage de carte défini - cf. la 'grande' carte et la carte du centre.
Si c'est le cas, alors nous réinitialisons les coordonnées des extend et nous appelons la fonction getMarker().
Avant de commencer à aller chercher les éléments en base de données, il convient de préparer le terrain à la construction du fichier XML.
Tout d'abord, définir le pas, l'extend de la carte (cf. côté client) et le nombre max d'objets par cellule à partir duquel on affiche un marqueur clusterisé :
Notons que nous avons défini également l'extend de carte supplémentaire dans le côté client afin de gérer l'événement sur le déplacement à la souris.
Puis vient le tour des paramètres passés via la fonction GDownloadUrl côté client - les bords de la carte - et profitons-en pour calculer l'extend réel :
Une fois toutes les étapes précédentes effectuées, il ne reste plus qu'à éditer le fichier XML correspondant à la requête passée côté client.
Le processus est simple : pour chaque cellule - dont nous avons maintenant les bornes - calculer le nombre d'éléments qu'elle contient puis ajouter des marqueurs dans le XML - soit un cluster calculé au centroïde des points de la cellule ou les marqueurs si le nombre de marqueurs dans la cellule est inférieur à la variable définie un peu plus haut.
Là j'avoue n'avoir pas été super efficace : je calcule 4 chaînes de caractères qui définissent les coordonnées de la cellule pour PostGIS. C'est pas la meilleure solution : il y a moyen de se passer du GeometryFromText.
Puis construisons enfin le XML - on différencie le cas où le nombre d'objets par cellule est supérieur à la variable (où on construit un marqueur 'cluster' qui invitera à zoomer dans la partie cliente) :
$res=pg_query($sql)ordie(pg_last_error());$nb_object=pg_num_rows($res);if($nb_object<>0){if($nb_object>$cluster_number){$node=$dom->createElement("marker");$newnode=$parnode->appendChild($node);$polygone="POLYGON((";$a=0;while($result=pg_fetch_array($res)){if($a==0){$firstPoint=$result['x']." ".$result['y'];}$polygone.=$result['x']." ".$result['y'].",";$a++;}$polygone.=$firstPoint;$polygone.="))";$sql2="select x(Centroid(GeometryFromText('".$polygone."',4326))) as x, y(Centroid(GeometryFromText('".$polygone."',4326))) as y";$res2=pg_query($sql2)ordie(pg_last_error());$result2=pg_fetch_array($res2);$lat=$result2['y'];$lng=$result2['x'];$newnode->setAttribute("lat_object",$lat);$newnode->setAttribute("lng_object",$lng);$newnode->setAttribute("nb_marker",$nb_object);$newnode->setAttribute("cluster",1);}else{while($result=pg_fetch_array($res)){$node=$dom->createElement("marker");$newnode=$parnode->appendChild($node);$newnode->setAttribute("lat_object",$result['y']);$newnode->setAttribute("lng_object",$result['x']);$newnode->setAttribute("title_object",stripslashes($result['title_object']));$newnode->setAttribute("cluster",0);}}}
Pour la construction du XML se reporter au tutoriel n°10.
Il suffit pour conclure de fermer le XML et de l'écrire :
<!DOCTYPE html "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><htmlxmlns="http://www.w3.org/1999/xhtml"><head><title> [Google Maps] 19. Clusterisation côté serveur
</title><styletype="text/css">html{overflow:hidden;height:100%;}body{height:100%;margin:0;}#map{width:100%;height:100%;}</style><linkrel="icon"type="image/png"href="./favicon.png"/><scriptsrc="http://maps.google.com/maps?file=api&v=2&key=votre_clé_ici"type="text/javascript"></script><scripttype="text/javascript">varmap;varextendPercent=50/100;functioncreateMarker(lat,lng,title,icon,cluster){varpoint=newGLatLng(lat,lng);if(!cluster){icon.image="./icon/marker/pic.png";icon.shadow="./icon/marker/shadow.png";varmark=newGMarker(point,{icon:icon,title:title});varinfowindow=title;GEvent.addListener(mark,'click',function(){mark.openInfoWindowHtml(infowindow);});map.addOverlay(mark);}else{icon.image="./icon/marker/cluster_marker.png";icon.shadow="./icon/marker/shadow.png";varmark=newGMarker(point,{icon:icon,title:title});varinfowindow="";GEvent.addListener(mark,'click',function(){map.setCenter(mark.getLatLng(),map.getZoom()+1);});map.addOverlay(mark);}}functiongetMarker(){map.clearOverlays();varmaxY=map.getBounds().getNorthEast().lat();varminY=map.getBounds().getSouthWest().lat();varmaxX=map.getBounds().getNorthEast().lng();varminX=map.getBounds().getSouthWest().lng();varzoomLevel=map.getZoom();varurlstr="./function/getObj.php?maxY="+maxY+"&minY="+minY+"&maxX="+maxX+"&minX="+minX+"&zoomLevel="+zoomLevel;GDownloadUrl(urlstr,function(data,responseCode){if(responseCode==200){varxmlDoc=GXml.parse(data);varmarkers=xmlDoc.documentElement.getElementsByTagName("marker");varicon=newGIcon();icon.iconSize=newGSize(24.0,38.0);icon.shadowSize=newGSize(44.0,38.0);icon.iconAnchor=newGPoint(12.0,38.0);icon.infoWindowAnchor=newGPoint(12.0,19.0);for(i=0;i<markers.length;i++){varlatMarker=parseFloat(markers[i].getAttribute("lat_object"));varlngMarker=parseFloat(markers[i].getAttribute("lng_object"));if(markers[i].getAttribute("cluster")==0){vartitle=markers[i].getAttribute("title_object");createMarker(latMarker,lngMarker,title,icon,false);}else{vartitle=markers[i].getAttribute("nb_marker")+' items - zoom to view (click on marker)';createMarker(latMarker,lngMarker,title,icon,true);}}}});}functioninitialize(){if(GBrowserIsCompatible()){map=newGMap2(document.getElementById('map'));map.setCenter(newGLatLng(42.57691664771851,0.402451992034912),9);map.addControl(newGMapTypeControl());map.removeMapType(G_HYBRID_MAP);map.addMapType(G_PHYSICAL_MAP);map.setMapType(G_PHYSICAL_MAP);map.addControl(newGOverviewMapControl());map.addControl(newGScaleControl());map.addControl(newGLargeMapControl());map.enableScrollWheelZoom();getMarker();varcenterLat=map.getCenter().lat();varcenterLng=map.getCenter().lng();varnorth=map.getBounds().getNorthEast().lat();varsouth=map.getBounds().getSouthWest().lat();varwest=map.getBounds().getNorthEast().lng();vareast=map.getBounds().getSouthWest().lng();GEvent.addListener(map,'moveend',function(){varnorth=map.getBounds().getNorthEast().lat();varsouth=map.getBounds().getSouthWest().lat();varwest=map.getBounds().getNorthEast().lng();vareast=map.getBounds().getSouthWest().lng();varcenterMoveLat=map.getCenter().lat();varcenterMoveLng=map.getCenter().lng();varextendY=Math.abs(north-south)*extendPercent;varextendX=Math.abs(west-east)*extendPercent;if((centerMoveLng>(centerLng+extendX))||(centerMoveLng<(centerLng-extendX))){centerLat=map.getCenter().lat();centerLng=map.getCenter().lng();north=map.getBounds().getNorthEast().lat();south=map.getBounds().getSouthWest().lat();west=map.getBounds().getNorthEast().lng();east=map.getBounds().getSouthWest().lng();getMarker();}if((centerMoveLat>(centerLat+extendY))||(centerMoveLat<(centerLat-extendY))){centerLat=map.getCenter().lat();centerLng=map.getCenter().lng();north=map.getBounds().getNorthEast().lat();south=map.getBounds().getSouthWest().lat();west=map.getBounds().getNorthEast().lng();east=map.getBounds().getSouthWest().lng();getMarker();}});GEvent.addListener(map,'zoomend',function(){getMarker();});}else{alert('Désolé, mais votre navigateur n\'est pas compatible avec Google Maps');}}</script></head><bodyonload="initialize()"onunload="GUnload()"><divid="map"></div></body></html>
<?phppg_connect("host=localhost dbname=****** user=****** password=******");$extendPercent=50/100;$cluster_number=5;$pas=10;$addExtendY=($_GET['maxY']-$_GET['minY'])*$extendPercent;$addExtendX=($_GET['maxX']-$_GET['minX'])*$extendPercent;($_GET['maxY']>0)?((($_GET['maxY']+$addExtendY)>90)?$maxY=90:$maxY=$_GET['maxY']+$addExtendY):$maxY=$_GET['maxY']-$addExtendY;($_GET['maxX']>0)?((($_GET['maxX']+$addExtendX)>180)?$maxX=180:$maxX=$_GET['maxX']+$addExtendX):$maxX=$_GET['maxX']-$addExtendX;($_GET['minY']>0)?$minY=$_GET['minY']-$addExtendY:((($_GET['minY']-$addExtendY)<-90)?$minY=-90:$minY=$_GET['minY']-$addExtendY);($_GET['minX']>0)?$minX=$_GET['minX']-$addExtendX:((($_GET['minX']-$addExtendX)<-180)?$minX=-180:$minX=$_GET['minX']-$addExtendX);$dom=newDomDocument('1.0','iso-8859-1');$node=$dom->createElement("markers");$parnode=$dom->appendChild($node);$largeur=$maxX-$minX;$hauteur=$maxY-$minY;$pas_largeur=$largeur/$pas;$pas_hauteur=$hauteur/$pas;for($i=0;$i<$pas;$i++){for($j=0;$j<$pas;$j++){$temp1=$minY+$pas_hauteur*$i;$temp2=$minX+$pas_largeur*$j;$temp3=$minY+$pas_hauteur*($i+1);$temp4=$minX+$pas_largeur*($j+1);$coord1=strval($temp2)." ".strval($temp1);$coord2=strval($temp4)." ".strval($temp1);$coord3=strval($temp4)." ".strval($temp3);$coord4=strval($temp2)." ".strval($temp3);$sql="SELECT x(geom_object) as x, y(geom_object) as y, title_object FROM object WHERE ";$sql.="Contains (GeometryFromText('POLYGON(($coord1,$coord2,$coord3,$coord4,$coord1))',4326),object.geom_object)";$res=pg_query($sql)ordie(pg_last_error());$nb_object=pg_num_rows($res);if($nb_object<>0){if($nb_object>$cluster_number){$node=$dom->createElement("marker");$newnode=$parnode->appendChild($node);$polygone="POLYGON((";$a=0;while($result=pg_fetch_array($res)){if($a==0){$firstPoint=$result['x']." ".$result['y'];}$polygone.=$result['x']." ".$result['y'].",";$a++;}$polygone.=$firstPoint;$polygone.="))";$sql2="select x(Centroid(GeometryFromText('".$polygone."',4326))) as x, y(Centroid(GeometryFromText('".$polygone."',4326))) as y";$res2=pg_query($sql2)ordie(pg_last_error());$result2=pg_fetch_array($res2);$lat=$result2['y'];$lng=$result2['x'];$newnode->setAttribute("lat_object",$lat);$newnode->setAttribute("lng_object",$lng);$newnode->setAttribute("nb_marker",$nb_object);$newnode->setAttribute("cluster",1);}else{while($result=pg_fetch_array($res)){$node=$dom->createElement("marker");$newnode=$parnode->appendChild($node);$newnode->setAttribute("lat_object",$result['y']);$newnode->setAttribute("lng_object",$result['x']);$newnode->setAttribute("title_object",stripslashes($result['title_object']));$newnode->setAttribute("cluster",0);}}}}}$xmlfile=$dom->saveXML();echo$xmlfile;?>
Le serveur hébergeant la démonstration n'étant plus disponible depuis de nombreuses années, la démonstration, autre fois intégrée en iFrame est désactivée. <iframe src="http://geotribu.net/applications/cluster/%5Bgeotribu%5D_Google_Maps_tuto19.html" height="350px" width="550px"></iframe>
Ce tutoriel utilise une base de données spatiale sous PostgreSQL et sa composante PostGIS. Ce couple permet de faire des requêtes spatiales - ici inclusion et calcul de centroïde. Le calcul du centroïde dans le cas d'une cellule contenant beaucoup d'objets ralentit un peu l'application, il aurait été plus rapide (mais moins joli :-) ) de placer le marqueur cluster simplement au milieu de la cellule.
Ce tutoriel montre qu'il est possible en peu de lignes de code, d'afficher intelligemment des données stockées en base. En effet, il est communément admis qu'à partir d'une centaine d'objets construits et affichés, les interfaces d'affichage cartographique Javascript (Google Maps ici, mais c'est sensiblement pareil pour OpenLayers) n'arrivent plus à être performantes. Il faut donc 'jouer' avec le nombre d'objets à afficher au maximum - ici en adaptant les paramètres d'extent, de pas et le nombre d'objets par cellule. Ce tutoriel n'est qu'une méthode parmi d'autres et ne se veut pas la plus optimale possible - notamment côté serveur ou les requêtes ne sont pas les plus efficaces.
Informaticien et géographe de formation, je suis j'étais indépendant en base de données, webmapping, j'aide à la modélisation et à la définition du besoin client, je développe j'encadre le développement des interfaces d'administration et des carto-web, je conçois des api géographiques pour les administrations, les ONG, les labos de recherche. Je suis maintenant responsable de l'innovation chez Sogefi. Toujours des cartes et des données qui ont besoin de communiquer. Nous avons une spécialité dans les données foncières et de leur représentation à destination des administrations.
J'aime comprendre les données et en faire quelquechose ; du coup je fais pas mal de veille techno sur tout ce qui touche aux technologies web de dataviz - carto ou non.
J'ai une préférence pour les technologies libres. Tout ce qui touche aux interactions hommes-machines avec des technos comme Kinect, Arduino, Raspberry, les imprimantes 3d ou les machines de découpe laser, les caméras 360° me passionne. Et j'essaie de les utiliser dans le cadre du travail.
Commentaires
Une version minimale de la syntaxe markdown est acceptée pour la mise en forme des commentaires. Propulsé par Isso.