Une metabox de Géolocalisation – v2

Il y a maintenant quelques temps, j’ai partagé un tutoriel expliquant comment concevoir une metabox pour calculer et associer des coordonnées GPS à un article. Mon approche était d’utiliser l’API de google maps pour collecter les informations à sauvegarder, en PHP…

Quelques lecteurs m’avaient fait remarquer que ce n’est pas ce qu’il y a de plus pratique car pour accéder aux points de coordonnées correspondants à une adresse il était forcément nécessaire de mettre à jour l’article en cours d’édition. Pour affiner la position il fallait donc s’y prendre à tâtons.

La metabox que j’utilise a donc évoluée grâce à vos remarques. À travers cet article je souhaite vous présenter ma méthode actuelle.

Une carte google map avec une metabox WordPress
Associer des coordonnées GPS à un article grâce à une metabox

Bien sûr, je précise que certains plugins tels qu’ACF font le job – si vous ne souhaitez pas coder – mais je pense qu’il est toujours utile et intéressant de comprendre comment cela fonctionne. Et puis, pour mettre en pratique le tutoriel sur les requêtes géolocalisées, seule cette metabox est adaptée 😉

Création de la metabox

Avant de commencer à écumer le code, je tiens à préciser que toutes les lignes de php ou de javascript de cet article iront dans deux fichiers : 1 php et 1 javascript. Libre à vous de placer ces fichiers dans votre thème ou bien dans un plugin 🙂

Toute metabox commence de la même façon : on emploie le hook add_meta_boxes et la fonction add_meta_box pour créer une nouvelle boîte qui s’ajoutera sur les pages d’édition du type de contenu souhaité. J’ai ici choisi de ne l’appliquer qu’aux posts, mais vous pouvez adapter cela en modifiant la condition php.

Ensuite on crée la fonction de callback (3e argument de add_meta_box). Celle-ci se contente de créer un input, et d’y pousser en valeur les coordonnées GPS potentiellement existantes. Pour esquiver les failles de sécurité CSRF, on ajoute un nonce qui sera vérifié au moment de la sauvegarde.

// Ajout de la metabox
add_action( 'add_meta_boxes', 'willy_load_metabox', 10, 2 );
function willy_load_metabox( $post_type, $post ) {
	if ( 'post' == $post_type ) {
		add_meta_box( 'willy-geo-coords', 'Coordonnées', 'willy_geo_metabox', $post_type, 'side', 'core' );
	}
}

// La metabox
function willy_geo_metabox( $post ) {
	$geo_lat  = get_post_meta( $post->ID, 'lat', true );
	$geo_lng  = get_post_meta( $post->ID, 'lng', true );

	$coords = $geo_lng && $geo_lat ? implode( ',', array( $geo_lat, $geo_lng ) ) : '';
	echo '<input type="text" id="geo-coords" name="geo-coords" value="' . esc_attr( $coords ) . '" style="width:100%;">';

	wp_nonce_field( 'geo-save_' . $post->ID, 'geo-nonce') ;
}

Sauvegarde de la metabox

Pour sauvegarder les coordonnées saisies, on se hook au moment du save_post. Tout d’abord nous vérifions qu’il s’agisse bien d’une sauvegarde volontaire de l’article, puis on regarde si $_GET contient notre champ avec des valeurs au format attendu.

Normalement, les valeurs devraient être de la forme (-)0.1234567,(-)0.1234567. On contrôle cela avec une regex.
Puis on vérifie le nonce, pour s’assurer que le referer est bien notre formulaire, et que c’est cet utilisateur qui l’a soumis.

Pour finir, on sauvegarde notre champ dans deux metadonnées distinctes, en utilisant la fonction update_post_meta.

// Sauvegarde de la metabox
add_action( 'save_post', 'willy_save_metabox' );
function willy_save_metabox( $post ) {
	if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
		return;
	}

	if ( isset( $_POST['geo-coords'] ) 
	  && preg_match( '/([0-9\.\-]+,[0-9\.\-]+)/', $_POST['geo-coords'] ) ) {

		check_admin_referer( 'geo-save_' . $_POST['post_ID'], 'geo-nonce' );

		$coords = array_map( 'trim', explode( ',', $_POST['geo-coords'] ) );
		update_post_meta( $_POST['post_ID'], 'lat', floatval( $coords[0] ) );
		update_post_meta( $_POST['post_ID'], 'lng', floatval( $coords[1] ) );
	}

Pourquoi sauvegarder la latitude et la longitude séparément ? Tout simplement car cela nous permettra de faire des requêtes géolocalisées, si ça vous intéresse rendez-vous ici !

Calculer les coordonnées en javascript via Leaflet

Au stade où nous en sommes, la metabox est déjà opérationnelle, vous pouvez la tester. Si vous saisissez des coordonnées GPS décimales et que vous mettez à jour l’article, celles-ci seront sauvegardées. Mais bon… c’est loin d’être intuitif !

Ce que nous allons faire maintenant, c’est utiliser une librairie javascript afin d’afficher une carte sur laquelle nous pourrons positionner et déplacer un marqueur pour récupérer des coordonnées GPS.

Récupérer les coordonnées au déplacement du pointeur

En premier lieu, nous allons avoir besoin de cette librairie. Celle que j’ai choisie s’appelle Leaflet et permet d’utiliser différents fournisseurs de cartes. Il faut donc l’enregistrer, ainsi que notre script, pour pouvoir les appeler dans notre metabox.

Si côté front, les scripts s’ajoutent via le hook wp_enqueue_scripts, côté back-office il faut s’accrocher sur admin_enqueue_scripts. Pensez à adapter l’URL de votre script pour que la metabox fonctionne.

Ensuite nous appelons ces scripts et styles directement dans la fonction de la metabox. Notre script aura besoin de connaître les valeurs existantes, ou initiales, des coordonnées. Nous les lui poussons grâce à la fonction wp_localize_script. Si jamais aucune valeur n’a été enregistrée, le marqueur sera initialisé à mon adresse.

Il ne faut pas oublier d’ajouter une <div> qui servira à afficher la carte.

// Ajout de scripts
add_action( 'admin_enqueue_scripts', 'willy_register_scripts' );
function willy_register_scripts() {
	wp_register_script( 'leaflet', 'http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js' );
	wp_register_script( 'geo-script', plugins_url( 'js/geo-script.js', __FILE__ ), array( 'leaflet', 'jquery' ) );
	wp_register_style( 'leaflet-css', 'http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css' );
}

// Modification de la metabox
function willy_geo_metabox( $post ) {
	$geo_lat  = get_post_meta( $post->ID, 'lat', true );
	$geo_lng  = get_post_meta( $post->ID, 'lng', true );

	// Coordonnées par défaut
	$map = array(
	        'lat' => $geo_lat ? floatval( $geo_lat ) : 47.09,
	        'lng' => $geo_lng ? floatval( $geo_lng ) : -1.39 );

	wp_enqueue_style( 'leaflet-css' );
	wp_enqueue_script( 'geo-script' );
	wp_localize_script( 'geo-script', 'currentCoords', $map );
	// La petite map leaflet ira ici
	echo '<div id="place-pointer" class="place-pointer" style="height:250px;width:100%;"></div>';

	$coords = $geo_lng && $geo_lat ? implode( ',', array( $geo_lat, $geo_lng ) ) : '';
	echo '<input type="text" id="geo-coords" name="geo-coords" value="' . esc_attr( $coords ) . '" style="width:100%;">';

	wp_nonce_field( 'geo-save_' . $post->ID, 'geo-nonce') ;
}

Une fois que l’on dispose de ces éléments, il ne reste plus qu’à concevoir notre javascript. Les méthodes mises à disposition par Leaflet pour créer une carte sont relativement simple, et relativement bien documentées.

Avec jQuery, on teste si notre <div> est présente dans la page. Si tel est le cas, nous créons une carte openstreetmap avec un pointeur dragable positionné exactement aux coordonnées que nous avons passées à notre script.

À chaque fois que le marqueur sera déplacé, les nouvelles coordonnées (que l’on limitera à 7 décimales) seront poussées dans notre input. La carte sera aussitôt centrée sur ce nouveau point.

jQuery( document ).ready(function( $ ) {
	if( $( '#place-pointer' ).length > 0 ) {
		// Création de la carte avec les coordonnées existantes…
		// … ou celles par défaut
		var map = L.map( 'place-pointer' ).setView( [ currentCoords['lat'], currentCoords['lng'] ], 13);
		L.tileLayer( 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' ).addTo( map );
		var fmarker = L.marker([ currentCoords['lat'], currentCoords['lng'] ], {
			draggable:true
		} ).addTo( map );

		// Lorsque l'on déplace le pointeur…
		// … mettre à jour notre input
		fmarker.on( 'dragend', function(event){
			var marker = event.target;
			var position = marker.getLatLng();
			map.setView( position, map.getZoom() );
			var coords = position.lat.toFixed(7) + ',' + position.lng.toFixed(7);
			$( '#geo-coords' ).value = coords;
		} );
	}
});

Notre metabox est déjà plus facile à utiliser, non ?

Déplacer le pointeur en saisissant une adresse

Plus simple oui, mais c’est pas encore ça… Il manque une fonctionnalité importante : pouvoir placer le marqueur sur la carte en saisissant une adresse.

Leaflet dispose d’extensions pour ajouter un champ de recherche à son UI, mais je ne les trouve pas particulièrement aboutis. Je vous propose donc de faire cela grâce à un input search qui adressera une requête ajax à l’API de geocoding de Google.

Ligne 13, ajoutons un nouveau champ de recherche dont la valeur ne sera pas sauvegardée puisqu’uniquement vouée à être utilisée par notre interface.

// Ajout d'un champ de recherche par adresse
function willy_geo_metabox( $post ) {
	$geo_lat  = get_post_meta( $post->ID, 'lat', true );
	$geo_lng  = get_post_meta( $post->ID, 'lng', true );

	$map = array(
	        'lat' => $geo_lat ? floatval( $geo_lat ) : 47.09,
	        'lng' => $geo_lng ? floatval( $geo_lng ) : -1.39 );

	wp_enqueue_style( 'leaflet-css' );
	wp_enqueue_script( 'geo-script' );
	wp_localize_script( 'geo-script', 'currentCoords', $map );
	echo '<input type="search" id="geo-search" placeholder="Adresse..." class="widefat">';
	echo '<div id="place-pointer" class="place-pointer" style="height:250px;width:100%;"></div>';

	$coords = $geo_lng && $geo_lat ? implode( ',', array( $geo_lat, $geo_lng ) ) : '';
	echo '<input type="text" id="geo-coords" name="geo-coords" value="' . esc_attr( $coords ) . '" style="width:100%;">';

	wp_nonce_field( 'geo-save_' . $post->ID, 'geo-nonce') ;
}

Pour finir, dans notre fichier javascript, nous allons écouter l’appui de la touche entrée dans ce champ. Lorsque l’utilisateur soumet une adresse, une requête ajax est envoyée à Google, qui nous répond en jSON.

Si cela vous intéresse, vous pouvez tester l’API et voir ce qu’elle renvoie, en ajoutant une adresse en paramètre GET à l’URL du service.

Dans ce retour, nous récupérons la latitude et la longitude du premier objet renvoyé, ce qui correspond aux coordonnées que Google estime être celles de l’adresse saisie. Il ne faut pas hésiter à saisir un code postal pour être sûr de l’adresse… (les villes ont des homonymes).

Ces informations, si elles existent, sont alors poussées dans notre input, le marqueur est positionné à l’endroit ciblé, et la carte est recentrée sur celui-ci.

jQuery( document ).ready(function( $ ) {
	if( $( '#place-pointer' ).length > 0 ) {
		var map = L.map( 'place-pointer' ).setView( [ currentCoords['lat'], currentCoords['lng'] ], 13);
		L.tileLayer( 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo( map );
		var fmarker = L.marker([ currentCoords['lat'], currentCoords['lng'] ], {
			draggable:true
		} ).addTo( map );

		fmarker.on( 'dragend', function(event){
			var marker = event.target;
			var position = marker.getLatLng();
			map.setView( position, map.getZoom() );
			var coords = position.lat.toFixed(7) + ',' + position.lng.toFixed(7);
			$( '#geo-coords' ).val( coords );
		} );
	
		// lorsque l'on presse la touche entrée sur notre champ de recherche…
		// … on envoie une requête ajax à l'API geocode de google maps…
		// … puis on déplace le pointer et on met à jour notre input
		$( '#geo-search' ).on( 'keydown', function( e ) {
			if ( e.keyCode === 13 ) { // 13 = touche entrée
				e.preventDefault();
				$.ajax({
					method: 'GET',
					url: 'https://maps.googleapis.com/maps/api/geocode/json',
					data: {
						address: $( this ).val()
					},
					success: function (data) {
						if ( typeof data.results[0].geometry.location != 'undefined' ) {
							var coords = data.results[0].geometry.location;
							var newLatLng = new L.LatLng( coords.lat, coords.lng );
							fmarker.setLatLng( newLatLng );
							map.setView( coords, map.getZoom() );
							coords = coords.lat.toFixed(7) + ',' + coords.lng.toFixed(7);
							$( '#geo-coords' ).val( coords );
						}
					}
				});
			}
		});
	}
});

Notre metabox est désormais pleinement opérationnelle !

Le code en entier

Pour ceux qui souhaite revoir le code dans son intégralité, le voici.

Le code PHP :

<?php
/*
 * Plugin Name: Geo metabox
 */

add_action( 'add_meta_boxes', 'willy_load_metabox', 10, 2 );
function willy_load_metabox( $post_type, $post ) {
	if ( 'post' == $post_type ) {
		add_meta_box( 'willy-geo-coords', 'Coordonnées', 'willy_geo_metabox', $post_type, 'side', 'core' );
	}
}

function willy_geo_metabox( $post ) {
	$geo_lat  = get_post_meta( $post->ID, 'lat', true );
	$geo_lng  = get_post_meta( $post->ID, 'lng', true );

	$map = array(
	        'lat' => $geo_lat ? floatval( $geo_lat ) : 47.09,
	        'lng' => $geo_lng ? floatval( $geo_lng ) : -1.39 );

	wp_enqueue_style( 'leaflet-css' );
	wp_enqueue_script( 'geo-script' );
	wp_localize_script( 'geo-script', 'currentCoords', $map );
	echo '<input type="search" id="geo-search" placeholder="Adresse..." style="width:100%;">';
	echo '<div id="place-pointer" class="place-pointer" style="height:250px;width:100%;"></div>';

	$coords = $geo_lng && $geo_lat ? implode( ',', array( $geo_lat, $geo_lng ) ) : '';
	echo '<input type="text" id="geo-coords" name="geo-coords" value="' . esc_attr( $coords ) . '" style="width:100%;">';

	wp_nonce_field( 'geo-save_' . $post->ID, 'geo-nonce') ;
}

add_action( 'save_post', 'willy_save_metabox' );
function willy_save_metabox( $post ) {
	if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
		return;
	}

	if ( isset( $_POST['geo-coords'] ) && trim( $_POST['geo-coords'] ) != '' ) {
		check_admin_referer('geo-save_'.$_POST['post_ID'], 'geo-nonce');

		$coords = array_map( 'trim', explode( ',', $_POST['geo-coords'] ) );
		update_post_meta( $_POST['post_ID'], 'lat', floatval( $coords[0] ) );
		update_post_meta( $_POST['post_ID'], 'lng', floatval( $coords[1] ) );
	}
}

add_action( 'admin_enqueue_scripts', 'willy_register_scripts' );
function willy_register_scripts() {
	wp_register_script( 'leaflet', 'http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js' );
	wp_register_script( 'geo-script', plugins_url( 'js/geo-script.js', __FILE__ ), array( 'leaflet', 'jquery' ) );
	wp_register_style( 'leaflet-css', 'http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css' );
}

Le script javascript :

jQuery( document ).ready(function( $ ) {
	if( $( '#place-pointer' ).length > 0 ) {
		var map = L.map( 'place-pointer' ).setView( [ currentCoords['lat'], currentCoords['lng'] ], 13);
		L.tileLayer( 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo( map );
		var fmarker = L.marker([ currentCoords['lat'], currentCoords['lng'] ], {
			draggable:true
		} ).addTo( map );

		fmarker.on( 'dragend', function(event){
			var marker = event.target;
			var position = marker.getLatLng();
			map.setView( position, map.getZoom() );
			var coords = position.lat.toFixed(7) + ',' + position.lng.toFixed(7);
			$( '#geo-coords' ).val( coords );
		} );
	
		$( '#geo-search' ).on( 'keydown', function( e ) {
			if ( e.keyCode === 13 ) {
				e.preventDefault();
				$.ajax({
					method: 'GET',
					url: 'https://maps.googleapis.com/maps/api/geocode/json',
					data: {
						address: $( this ).val()
					},
					success: function (data) {
						if ( typeof data.results[0].geometry.location != 'undefined' ) {
							var coords = data.results[0].geometry.location;
							var newLatLng = new L.LatLng( coords.lat, coords.lng );
							fmarker.setLatLng( newLatLng );
							map.setView( coords, map.getZoom() );
							coords = coords.lat.toFixed(7) + ',' + coords.lng.toFixed(7);
							$( '#geo-coords' ).val( coords );
						}
					}
				});
			}
		});
	}
});

Je rappelle d’ailleurs que tous les codes de tous les tutoriaux de Wabeo sont mis à disposition sur mon Bitbucket, n’hésitez pas à venir y jeter un oeil et à me soumettre vos idées d’améliorations.

1 commentaires
Adrien, il y a 8 mois
Genial ! Très bon article qui m’a servi pour un projet. Je ne connaissais pas Leaflet, c’est pratique et simple d’utilisation en effet. Merci.

Laisser un commentaire

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

Publié le 08 avril 2015
par Willy Bahuaud
Catégorie Développement WordPress