Playing - Liste des aéroports français

Leaflet & D3JS & Voronoi
d3js5.x leaflet1.3.1
Sources :

Introduction

Cette carte affiche la liste des aéroports français (les points rouges) superposée à un fond de carte Leaflet. Les cellules (polygones) sont construites à partir d'un découpage visant à ce que chaque point à l'intérieur d'une cellule soit le plus proche du point rouge qu'il contient. Dit autrement, si vous êtes quelque part en France et que vous devez au plus vite rejoindre un aéroport cette carte vous indique quel est le plus proche (à vol d'oiseau bien sûr). Plus une cellule est foncée plus l'aéroport qu'elle contient possède une piste longue, la plus courte étant celle de Corte en Corse avec 940m et la plus longue celle de Charles de Gaulle avec 4215m. Ce type de visualisation est appelée diagramme de Voronoï et voici la définition qu'en donne Wikipédia : En mathématiques, un diagramme de Voronoï est un découpage du plan (pavage) en cellules à partir d'un ensemble discret de points appelés « germes ». Chaque cellule enferme un seul germe, et forme l'ensemble des points du plan plus proches de ce germe que de tous les autres. La cellule représente en quelque sorte la « zone d'influence » du germe.

Création de la carte et du panneau d'information

Après avoir créé notre carte ce tutoriel introduit l'utilisation de la classe Leaflet Control qui permet d'ajouter divers contrôles à la carte. Les boutons pour zoomer/dézoomer et le bouton permettant de sélectionner les différentes couches d'une carte sont des exemples de contrôles. Ici nous construisons un contrôle basic via la fonction L.control(). Sur ce contrôle deux méthodes sont définies. La première (onAdd) ajoute un DIV au moment ou ce contrôle sera ajouté à la carte (ligne 30). La seconde (update) définit le comportement habituel d'un tooltip et vise à remplir le DIV avec les informations du polygone sur lequel l'utilisateur passe sa souris.

var stamenToner = L.tileLayer('http://stamen-tiles-{s}.a.ssl.fastly.net/toner/{z}/{x}/{y}.png', {
	attribution: 'Map tiles by Stamen Design, CC BY 3.0 - Map data © OpenStreetMap',
	subdomains: 'abcd',
	minZoom: 0,
	maxZoom: 20,
	ext: 'png'
});

var map = new L.Map("map", {
	center: new L.LatLng(46.90296, 1.90925),
	zoom: 6,
	layers: [stamenToner],
});

var info = L.control();

info.onAdd = function (map) {
	this._div = L.DomUtil.create('div', 'info');
	this.update();
	return this._div;
};

info.update = function (e) {
	if (e === undefined) {
		this._div.innerHTML = '<h4>Informations</h4>';
		return;
	}
	this._div.innerHTML = '<h4>Informations</h4>'
		+  '<span style="font-weight:bold;">' + e.airport
		+  '</span><br/>Code OACI : <span style="font-weight:bold;">' + e.oaci_code
		+  '</span><br/>Longueur de piste : <span style="font-weight:bold;">' + e.length + ' m'
		+  '</span><br/>Largeur de piste : <span style="font-weight:bold;">' + e.width + ' m'
		+  '</span><br/>Altitude : <span style="font-weight:bold;">' + e.high + ' m' + '</span>';
};

info.addTo(map);

Voici le CSS associé au panneau d'information.

.info {
	padding: 6px 8px;
	font: 14px/16px Arial, Helvetica, sans-serif;
	background: white;
	background: rgba(255,255,255,0.8);
	box-shadow: 0 0 15px rgba(0,0,0,0.2);
	border-radius: 5px;
	min-width: 200px;
}

.info h4 {
	margin: 0 0 5px;
	color: #777;
}

Gestion du zoom/dézoom

Afin de faire le lien entre Leaflet et l'action visant à zoomer ou dézoomer sur la carte et l'affichage via D3JS du diagramme de Voronoï nous devons recalculer ce dernier à chaque fois que la vue change (évènement viewreset ou moveend). C'est donc la fonction drawVoronoi qui assure la création du diagramme en fonction du niveau de zoom et de l'ensemble des points à afficher.

map.on("viewreset moveend", drawVoronoi);

Chargement du fichier TSV

Rien de très particulier ici si ce n'est que le fichier possède plusieurs pistes pour le même aéroport, nous ne gardons que la première (c'est aussi la plus longue). Le fichier peut être récupéré ici : french-airport.csv

// Contenu du fichier :
// airport	oaci_code	length	width	high	latitude	longitude
// Abbeville	LFOI	1250	29	67	50.140	1.830
// Aire sur l'adour	LFDA	1000	29	78	43.710	-0.250
// ...
var points = [];
d3.tsv("playing/leaflet-voronoi/french-airport.csv", function(error, data) {
	// Filter duplicate entries with same location but different runway
	var added = [];
	points = data.filter(function(d) {
		var alreadyAdded = false;
		for (var idx = 0; idx < added.length; idx++) {
			if (added[idx] == ('' + d.latitude + d.longitude)) {
				alreadyAdded = true;
			}
		}
		added.push('' + d.latitude + d.longitude);
		return alreadyAdded == false;
	});
	drawVoronoi();
});

C'est la dernière ligne qui va déclencher l'appel à la fonction drawVoronoi.

drawVoronoi

En lisant ce code commenté il faut bien avoir en tête qu'il sera appelé chaque fois que l'on déplace la position de la carte ou que l'on utilise la fonction de zoom.

function drawVoronoi() {
	// On commence par déterminé les coordonnées du rectangle dans lequel on se situe
	var bounds = map.getBounds(),
		topLeft = map.latLngToLayerPoint(bounds.getNorthWest()),
		bottomRight = map.latLngToLayerPoint(bounds.getSouthEast()),
		drawLimit = bounds.pad(0.4);

	// On filtre nos points pour ne représenter que ceux qui sont visibles dans le rectangle
	// On définit d.x et d.y, la position en pixel de l'aéroport en fonction de sa latitude/longitude
	filteredPoints = points.filter(function(d) {
		var latlng = new L.LatLng(+d.latitude, +d.longitude);
		if (!drawLimit.contains(latlng)) {
			return false 
		};

		var point = map.latLngToLayerPoint(latlng);
		d.x = point.x;
		d.y = point.y;
		return true;
	});
	
	// On calcule notre intervalle de couleurs à partir de la longueur de piste
	var maxLength = d3.max(filteredPoints, function(e) { return +e.length; });
	var color = d3.scaleLinear()
		.domain([0, maxLength])
		.range(["rgb(255,245,235)", "rgb(127,39,4)"]);

	// Comme toujours avec D3JS lorsqu'un type de graphique a été intégré, il est très
	// facile à mettre en oeuvre. la fonction voronoi appliquée sur la liste des points
	// filtrés ajoutent pour chacun d'eux le polygone que l'on va représenter. Par 
	// sécurité on ne conserve que les points définis dans readyVoronoiPolygons.
	var voronoi = d3.voronoi()
		.x(function(d) { return d.x; })
		.y(function(d) { return d.y; })
		.extent([[topLeft.x, topLeft.y], [bottomRight.x, bottomRight.y]]);
	
	var voronoiPolygons = voronoi.polygons(filteredPoints);
	var readyVoronoiPolygons = [];
	for (let i = 0; i < voronoiPolygons.length; ++i) {
		if (voronoiPolygons[i] !== undefined) {
			readyVoronoiPolygons.push(voronoiPolygons[i]);
		}
	}
	
	// On supprime le résultat d'un précédent appel puis on ajoute notre SVG à la carte
	d3.select("svg").remove();
	var svg = d3.select(map.getPanes().overlayPane).append("svg")
		.attr("id", "overlay")
		.attr("class", "leaflet-zoom-hide")
		.style("width", map.getSize().x + "px")
		.style("height", map.getSize().y + "px")
		.style("margin-left", topLeft.x + "px")
		.style("margin-top", topLeft.y + "px");

	// On construit un groupe pour les polygones et un groupe pour les cercles représentant les aéroport.
	var pathGroup = svg.append("g")
		.attr("transform", "translate(" + (-topLeft.x) + "," + (-topLeft.y) + ")");
	var cirlceGroup = svg.append("g")
		.attr("transform", "translate(" + (-topLeft.x) + "," + (-topLeft.y) + ")");

	// Cette fonction nous permettra de dessiner le polygone à partir du calcul de la fonction voronoï
	var buildPathFromPoint = function(point) {
		return "M" + point.cell.join("L") + "Z";
	}

	// On ajoute chaque polygone en associant sa couleur et l'évènement mouseover pour mettre à jour les données du panneau d'information
	pathGroup.selectAll("cell")
		.data(readyVoronoiPolygons)
		.enter()
			.append("path")
			.attr("class", "cell")
			.attr("d", function(d) { return "M" + d.join("L") + "Z" }) // Dessine le polygone à partir du calcul de la fonction voronoï
			.attr("fill", function(d) { return color(d.data.length); })
			.on("mouseover", function(d) {info.update(d.data); });

	// De la même façon on ajoute un cercle rouge pour chaque aéroport
	cirlceGroup.selectAll("circle")
		.data(filteredPoints)
		.enter()
			.append("circle")
			.attr("class", "point")
			.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
			.attr("r", 2);
}

Voici le CSS associé aux classes cell et point. Il est impératif de définir pointer-events sans quoi les évènements de la souris ne seront pas associés aux polygones.

.cell {
	stroke: white;
	opacity: 0.6;
	pointer-events: visible !important;
}
		
.cell:hover {
	stroke: black;
	stroke-width: 5px;
}

.point {
	fill: red;
}

Une fois n'est pas coutûme pour cette section, l'ensemble du code a été présenté. Gardez en tête que si vous avez plus de 500 points, le recalcule du diagramme de Voronoï commencera a ralentir un peu l'interface.

COMMENTAIRES