Les articles se font rares sur mon blog ces derniers temps, non pas que j’ai perdu la motivation (au contraire…) mais justement, le temps passé à développer n’est malheureusement pas du temps passé à écrire ! Et puis… il faut bien apprendre avant de partager non ?
Ce n’est pas vrai non plus, je ne passe pas mon temps à développer (loin de là), j’ai aussi un nouveau boulot très prenant et enfin une vie sociale sur un réseau privée (et pas l’inverse).
L’objet de l’article d’aujourd’hui est l’utilisation de l’api GoogleMaps avec Rails à travers le plugin Ym4r. Comme un bon exemple vaut toujours mieux qu’un long discours, je vais reprendre mon application de gestion de CD et nous allons l’améliorer encore afin de positionner nos CD sur une carte GoogleMaps.
Ok, ok, c’est parfaitement inutile de positionner un CD sur une carte mais c’est pas grave, l’objectif encore une fois est de jouer avec cette API et non de faire une “killer application”. Ces détails réglés lançons-nous maintenant dans notre application.
Rappel sur ma première application Rails
Il y a quelques mois j’ai publié un long article présentant la création d’une application Rails depuis la première ligne de code jusqu’à son hébergement sur internet. Quelques temps plus tard je publiai un “addon” à cet article présentant l’ajout d’un système de nuage de tags et nous voilà aujourd’hui avec un nouvel ajout afin d’apprendre à manipuler les fameuses cartes de Google en toute simplicité…
Pourquoi utiliser comme base mon application ? Pour plusieurs raisons, tout d’abord ça m’évite de re-présenter certains principes que j’ai déjà développés auparavant, ensuite cela permet de voir comment intégrer une nouvelle fonctionnalité sans pour autant recommencer toute une application… et c’est bien l’objectif premier de Rails : pouvoir s’étendre sans tout recommencer !
Googlemaps oui mais …
faut se tapper toute l’Api avant de commencer ?
Et bien c’est là que c’est génial : NON ! Pas besoin de tout savoir pour se lancer, et c’est bien ce qui m’a attiré au départ… L’API GoogleMaps est une API Javascript à la base donc intégrable normalement avec n’importe quel langage que ce soit PHP, Ruby, Java, Perl et j’en passe. Cependant lorsque l’on veut utiliser une API directement il faut normalement passer par la case : “étude de l’api” … bof, bof ! Non pas que c’est pas intéressant mais comme tout le monde ce qui m’intéresse c’est de vite mettre le tout en application !
Une fois de plus nous allons nous appuyer sur l’une des force de Rails : ses plugins. Et oui… GoogleMaps, je suis pas le premier à m’y pencher sur Rails (loin de là !) et ce qui est bien c’est que d’autre ont pensé à nous faciliter la tâche.
Nous allons donc travailler avec le plugin Ym4r (en toute lettre Yahoo Maps For Ruby…). Je lève le doute sans attendre : non, nous n’allons pas jouer avec les carte Yahoo mais bien avec celle de Google mais le truc bien du plugin c’est que si demain vous voulez passer de Google à Yahoo Maps et bien le code sera toujours le même… sympa non ?
NB : pour les plus curieux, vous avez la doc ici : Documentation plugin YM4R
Voici donc ce que nous allons faire :
- Installation du plugin
- A la création d’un CD (action new), nous allons positionner le lieu de composition du CD sur la carte du monde
- A l’affichage de la liste des CDs nous allons afficher une carte avec tous les points correspondants à tous les CDs
Avec ces 2 actions on retrouve les cas les plus commun de la gestion de carte sur une application web…
I. Installation du plugin
Rien de bien nouveau, c’est très classique, un simple “script/plugin” install suffira :
script/plugin install svn://rubyforge.org/var/svn/ym4r/Plugins/GM/trunk/ym4r_gm
Il faut ensuite le configurer correctement, en effet, pour utiliser l’api google, il faut obtenir une clé. Pour cela rendez-vous à cette adresse : http://www.google.com/apis/maps/signup.html, en bas de la page cochez la case puis saisissez l’adresse de votre site (http://localhost:XXXX ou autre) puis validez, vous obtiendrez un numéro à “coller” dans le fichier (config/gmaps_api_key.yml).
II. Nouveau CD : positionnement sur la carte.
Pour que le positionnement soit ergonomique, nous allons le faire en 2 temps :
- Saisie d’une adresse et affichage de cette adresse sur la carte en Ajax
- Déplacement du point sur la carte présentée
Accrochez vous bien cette partie va être longue, nous allons initialiser tout le plugin et faire en sorte de ne rien oublier pour que ça marche !
Sur notre vue (views/cd/_form.rhtml), il faut prévoir un encart pour afficher la carte, et une zone de formulaire pour saisir l’adresse à localiser :
...
<p><label for="cd_tag_list">Genres (tags)</label><br/>
<%=text_field 'cd', 'tag_list'%></p>
<p><label for="cd_address">Address</label><br/>
<%=text_field 'cd', 'address'%></p>
<%=@map.to_html %>
<%=@map.div() %>
Dans notre layout (views/layouts/cd.rhtml) il faut ajouter les headers correspondant au plugin :
...
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<%= stylesheet_link_tag 'scaffold' %>
<%= javascript_include_tag :defaults %>
<%= GMap.header %> #ICI !
</head>
...
Enfin dans le css (public/scaffold.css) il faut ajouter un cadre pour notre carte :
/* Google Map */
#mymap{
width:600px;
height:400px;
}
Ok, on a ajouter un champs “address” cependant il faut que notre modèle le sache, nous allons donc faire une migration pour ajouter ce champs :
script/generate migration add_address_to_cd
Voici le contenu de notre migration :
class AddAddressToCd < ActiveRecord::Migration
def self.up
add_column :cds, :address, :string
end
def self.down
remove_column :cds, :address
end
end
Un petit tour par la console pour lancer un :
rake db:migrate
Commençons maintenant par afficher la carte dans le cas de la création d’un nouveau CD, éditez votre cd_controller, action “new” :
def new
@map = GMap.new("mymap") # nom du div contenant la carte
@map.control_init(:small_map => true, :map_type => true,:small_zoom=> false ,:overview_map=>true) # paramètres de la carte
@latitude=46.12
@longitude=2.59
@map.center_zoom_init([@latitude, @longitude],5) # centrage
@cd = Cd.new
end
Et nous voilà prêt pour jeter un oeil sur notre navigateur. Allez sur votre adresse (localhost:XXXX en général) puis tentez de créer un nouveau CD, vous devriez voir le champs “Adresse” et en dessous la fameuse Google Map… Maintenant nous allons faire en sorte que lorsque l’utilisateur saisis une adresse dans le champs ‘Address” elle soit affichée automatiquement sur la carte en dessous !
L’idée est donc de créer un bouton “Visualiser” à côté du champs de saisie de l’adresse. Au clic sur ce bouton, une action du controller appelée en Ajax mettra à jour la carte afin d’afficher l’adresse saisie si elle est valide… Enfin, lorsque le point sera positionné correctement il faudra enregistrer les coordonnées “GPS” pour pouvoir l’afficher ultérieurement. Un petit dicton rails pour l’occasion : Facile à dire, facile à faire !
On reprends donc le code de cd/_form.rhtml et on ajoute le bouton, et les champs address, longitude et latitude pour enregistrer l’adresse correcte (dans une utilisation normale on préfèrera un hidden_field pour ne pas afficher les coordonnées GPS) :
...
<p><label for="address">Address</label><br/>
<%=text_field_tag 'address'%>
<%=link_to_remote('Visualiser !',:url => {:controller =>"cd",:action => "visualiser"},:with => "'address=' + $F('address') + '&name=' + $F('cd_title')")%>
<br/>
<%=text_field 'cd', 'address'%>
<%=text_field 'cd', 'longitude', :value=>@longitude.to_s%>
<%=text_field 'cd', 'latitude', :value=>@latitude.to_s%>
</p>
...
NB : Le ‘& # 0 3 8 ;’ est à remplacer par un simple ‘&’, faudra vraiment que je passe de markdown à textile…
Donc pour répondre à ce bouton, il nous faut une action correspondante dans notre cd_controller.rb :
def visualiser
@results = Geocoding::get(params[:address])
@map = Variable.new('map')
if @results.status == Geocoding::GEO_SUCCESS
@address=@results[0].address
@longitude=@results[0].longitude
@latitude=@results[0].latitude
@title=params[:title]
@center="map.setCenter(new GLatLng(#{@results[0].latitude}, #{@results[0].longitude}), 16);"
@marker = GMarker.new(@results[0].latlon,:draggable => "true")
render :action => "visualiser.rjs"
else
render :action => "visualiserNOK.rjs"
end
end
Le rôle de cet action est le suivant :
- Interroger Google via l’api afin de localiser l’adresse
- Si l’adresse est correctement trouvée alors on assigne les variables avec les valeurs récupérées, on centre la carte sur les positions de l’adresse et enfin on place un point sur la carte à l’adresse correspondante, ce point est défini comme “draggable”, c’est à dire qu’il pourra être bougé.
- Enfin on exécute le RJS visualiser.rjs. (et pourquoi choisir un RJS comme “vue” ? … Bah… Pourquoi pas ?
En fait nous allons manipuler la page avec en javascript et le RJS s’y prête très bien…) (Si l’adresse n’est pas trouvée c’est un autre vue qui sera “rendue” : visualiserNOK.rjs)
Maintenant il ne reste qu’à écrire nos 2 rjs :
visualiser.rjs (explications en commentaire) :
#On efface ce qui traine sur la carte...
page << @map.clear_overlays
#On déclare notre marker
page << @marker.declare("marker")
#On détermine une suite d'action sous forme d'une fonction en réponse à l'évènement dragend de notre marker
#En d'autres termes, la suite d'action sera exécutée lorsque le point sur la carte sera déplacé.
page << @marker.on_dragend("function () {
#On récupère donc le point d'arrivé (la ou on relache notre marker)
#Ensuite on assigne aux champs du formulaire les coordonnées du point
#afin que ces coordonnées soient sauvegardées si nous enregistrons le formulaire maintenant.
point = marker.getPoint();
map.savePosition();
$('cd_latitude').value = point.lat();
$('cd_longitude').value = point.lng();
return false;
}")
#J'ajoute maintenant le marker sur la carte
page << @map.add_overlay(@marker)
#Je centre (ok ça c'est un commentaire qui sert à rien)
page << @center
#J'assigne à mes champs de formulaire les variables définies dans mon action ci dessus
page[:address].value = @address
page[:cd_address].value = @address
page[:cd_longitude].value = @longitude
page[:cd_latitude].value = @latitude
#Enfin pour la compréhension du formulaire, je change la police du champs adresse en vert car la localisation
#a été faite avec succès.
page << "Element.setStyle('cd_address', {color: 'green '})"
visualiserNOK.rjs (rien de spécial ici, je colore juste en rouge avec un effet “highlight” si l’adresse n’est pas localisée, je vide aussi le champs cd_address, ça aura son importance plus tard…) :
page << @map.clear_overlays
page[:cd_address].value = ''
page[:address].visual_effect :highlight, :duration => 2
page << "Element.setStyle('address', {color: 'red '})"
Bon, ça fait un petit moment qu’on est pas allé voir notre navigateur préféré : tentez donc de créer un nouveau CD, saisissez une adresse puis cliquez sur “vérifier”, si tout va bien et si l’adresse est bonne vous devriez voir la carte se positionner au bon endroit…
Je vous invite donc à remplir votre base de donnée de test en créant 4 ou 5 cds avec des adresses… Pour ceux qui suivent le tuto depuis le début et qui veulent modifier leur jeu d’essai existant il faut avant tout modifier les actions edit/update de notre cd_controller.rb :
def edit
@map = GMap.new("mymap")
@map.control_init(:small_map => true, :map_type => true,:small_zoom=> false ,:overview_map=>true)
@latitude=46.12
@longitude=2.59
@map.center_zoom_init([@latitude, @longitude],5)
@cd = Cd.find(params[:id])
end
def update
@cd = Cd.find(params[:id])
if @cd.update_attributes(params[:cd])
flash[:notice] = 'Cd was successfully updated.'
redirect_to :action => 'show', :id => @cd
else
@map = GMap.new("mymap")
@map.control_init(:small_map => true, :map_type => true,:small_zoom=> false ,:overview_map=>true)
@latitude=46.12
@longitude=2.59
@map.center_zoom_init([@latitude, @longitude],5)
render :action => 'edit'
end
end
Vous avez surement remarqué qu’il y a un bloc de code que l’on répète plusieurs fois dans notre controller :
@map = GMap.new("mymap")
@map.control_init(:small_map => true, :map_type => true,:small_zoom=> false ,:overview_map=>true)
@latitude=46.12
@longitude=2.59
@map.center_zoom_init([@latitude, @longitude],5)
Ce bloc correspond à l’initialisation de la carte, c’est pas très “Rails” de répéter ça à chaque fois, mais pour l’exemple on va dire que c’est bien
Libre à vous donc de vous faire un petite fonction en partie privée de la classe CdController pour placer ce code et ensuite appeler la fonction à chaque fois que vous en avez besoin…
II. Liste des CDs : carte globale
Ok, si nous revenons vers notre action “list” : notre base de donnée est bien remplie avec des adresses (et donc des coordonnées LATLON) on a donc un affichage de tout nos CDs, pourquoi pas ne pas afficher une carte globale en positionnant chaque élément de la liste ?
Commençons par prévoir un encart sur notre page pour positionner une carte, il faut donc ajouter les lignes suivantes (ou vous voulez) dans notre views/cd/list.rhtml :
<h1>Cd Map </h1>
<%=@map.to_html %>
<%=@map.div() %>
Ensuite il faut déclarer la carte dans notre cd_controller.rb, pour cela nous allons encore répéter le code d’initialisation :
def list
@cd_pages, @cds = paginate :cds, :per_page => 10
@tags = Cd.tag_counts(:order=>'tags.name asc')
@map = GMap.new("mymap")
@map.control_init(:small_map => true, :map_type => true,:small_zoom=> false ,:overview_map=>true)
@latitude=46.12
@longitude=2.59
@map.center_zoom_init([@latitude, @longitude],5)
end
Faites un tour sur http://localhost:XXXX/cd/list, vous verrez que la carte apparait correctement. Reste maintenant à placer chaque CD sur la carte et cela va se faire dans notre action list du controller, mais avant un peu d’explication…
En effet nous allons placer des points sur une carte et… cela peut poser quelques problèmes : si nous avons 10 CDs dans notre discothèque il n’y aura pas trop de problèmes pour les afficher. Par contre, si nous en avons 5000 il est difficile d’imaginer une petite carte Google Maps affichant 5000 CDs, l’idéal serait alors d’afficher des points représentant des groupes de CDs qui sont proches. Toujours dans l’idéal, plus l’utilisateur zomera sur la carte plus les points affichés représenteront des ensembles de CDs plus petits jusqu’à finalement afficher les CDs réels dans les derniers niveaux de zoom. Encore une fois l’idéal serait que nous n’ayons pas à nous soucier du niveau de zoom ni du nombre de CDs dans notre discothèque…
Si je vous dis que tout ça c’est possible, si je vous dis en plus que c’est super simple, et si je dis qu’il y a de la doc ici pour le faire et si pour couronner le tout je vous donne un exemple, j’ai bon ?
En fait il existe plusieurs façons d’afficher un ensemble de points en utilisant le plugin, soit directement en initialisant chaque point puis en l’ajoutant à la carte (comme dans ma première partie en utilisant la classe GMarker), soit en utilisant les groupe de points (classe GMarkerGroup) et enfin en utilisant les clusters (classe Clusterer). Je lève le suspens sans tarder, j’utilise les clusters. Pourquoi ? Parcequ’ils permettent tout ce que je vous ai présenté ci dessus…
La librairie javascript ‘clusterer’ n’est pas disponible par défaut dans le plugin, il faut donc la “charger” dans notre layout ‘cd.rhtml’ (/views/layout/cd.rhtml) pour pouvoir utiliser ses fonctions :
<%= javascript_include_tag :defaults %>
<%= GMap.header %>
<%= javascript_include_tag 'clusterer' %>
Ensuite, il ne nous reste qu’à écrire l’affichage des points (et de leurs cluster) dans notre action ‘list’, lisez les commentaires si le code ne vous suffit pas :
def list
@cd_pages, @cds = paginate :cds, :per_page => 10
@tags = Cd.tag_counts(:order=>'tags.name asc')
@map = GMap.new("mymap")
@map.control_init(:small_map => true, :map_type => true,:small_zoom=> false ,:overview_map=>true)
@latitude=46.12
@longitude=2.59
@map.center_zoom_init([@latitude, @longitude],5)
# création d'un tableau qui contiendra nos points
markers = Array.new
@cds.each do |cd|
#on ajoute des points à notre tableau
markers << GMarker.new([cd.latitude,cd.longitude], :title => cd.title, :info_window => cd.title+"<br /> "+cd.description[0..50]+"...<br/>"+cd.address)
end
#on crée notre cluster à partir de notre tableau de points (markers)
#il y a des parametres pour définir les options du cluster
# - max_visible_markers : défini le nombre maximum de cluster visibles sur la carte
# - min_markers_per_cluster : défini le nombre minimum de points dans un cluster
# - max_lines_per_info_box : défini le nombre max de lignes dans la boite d'info (celle qui apparait au clic sur un point)
clusterer = Clusterer.new(markers, :max_visible_markers => 20,:min_markers_per_cluster=>3,:max_lines_per_info_box=>3)
@map.overlay_init clusterer
end
The end
Ouch ! J’ai cru que je trouverai jamais le courage de finir cet article mais… voilà qui est fait ! Comme d’habitude je reste ouvert à toute remarque/rectification/correction.
Je n’ai pas pris le temps de le relire et tester entièrement l’article, on dira donc qu’il est en beta
En parlant de beta faudra que je vous montre un truc un de ces 4…






Très bon article. Vous avez fait mon innitiation à Ym4r, et ce fut un plaisir.
Merci Beaucoup.
Je crois que les lignes de commentaire dans le on_dragend font planter le script.