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