MediaWiki:Carte-osm.js
Apparence
Note : après avoir publié vos modifications, il se peut que vous deviez forcer le rechargement complet du cache de votre navigateur pour voir les changements.
- Firefox / Safari : maintenez la touche Maj (Shift) en cliquant sur le bouton Actualiser ou appuyez sur Ctrl + F5 ou Ctrl + R (⌘ + R sur un Mac).
- Google Chrome : appuyez sur Ctrl + Maj + R (⌘ + Shift + R sur un Mac).
- Edge : maintenez la touche Ctrl en cliquant sur le bouton Actualiser ou pressez Ctrl + F5.
/* Cartes OpenStreetMap de WikiColombes.
*
* Mode d'emploi, côté wikicode :
* - {{Carte OSM}} place la carte dans la page ;
* - {{Point carte|latitude|longitude|niveau=…|statut=…|adresse=…}}, posé à côté
* d'un nom dans une liste, y ajoute ce lieu.
* Le script lit tous les points de la page, les range sous le titre de section
* (niveau 3, à défaut 2) qui les précède, et fabrique la légende à partir de ces
* titres. Ajouter une ligne à la liste suffit donc à l'ajouter à la carte.
*
* Niveaux : M maternelle, É élémentaire, P primaire (couleur des écoles),
* C collège, L lycée ; plusieurs niveaux se séparent par des espaces (« P C L »).
* Sans niveau, le lieu reçoit une pastille neutre : la carte sert aussi hors écoles.
* statut=privé dessine une pastille carrée au lieu d'une ronde.
*
* Bibliothèque : Leaflet 1.9.4, hébergée sur le serveur du wiki
* (/ressources/leaflet/1.9.4/) pour ne dépendre d'aucun CDN tiers.
* Fond de carte : tuiles OpenStreetMap standard, attribution obligatoire.
* Styles : bloc « Cartes OpenStreetMap » de MediaWiki:Common.css.
*/
( function () {
'use strict';
if ( mw.carteOsm ) {
return; // déjà chargé (aperçu de modification, etc.)
}
mw.carteOsm = {};
var LEAFLET = '/ressources/leaflet/1.9.4/';
var TUILES = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
var ATTRIBUTION = '© <a href="https://www.openstreetmap.org/copyright">les contributeurs d’OpenStreetMap</a>';
var NIVEAUX = {
M: { nom: 'École maternelle', famille: 'ecole' },
'É': { nom: 'École élémentaire', famille: 'ecole' },
P: { nom: 'École primaire', famille: 'ecole' },
C: { nom: 'Collège', famille: 'college' },
L: { nom: 'Lycée', famille: 'lycee' },
'•': { nom: 'Lieu', famille: 'lieu' } // point sans niveau : pastille neutre
};
var ORDRE = [ 'M', 'É', 'P', 'C', 'L', '•' ];
var HAUTEUR_PX = 26; // hauteur d'un marqueur (pastille de 22 px + liseré)
var MARGE_PX = 3; // écart minimal entre deux marqueurs voisins
var reduit = window.matchMedia && window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches;
/* ---------- utilitaires ---------- */
function echapper( s ) {
return String( s ).replace( /[&<>"']/g, function ( c ) {
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ c ];
} );
}
function lireNiveaux( texte ) {
var vus = [];
String( texte || '' ).toUpperCase().split( /[\s,;\/]+/ ).forEach( function ( n ) {
if ( n === 'E' ) {
n = 'É';
}
if ( NIVEAUX[ n ] && vus.indexOf( n ) === -1 ) {
vus.push( n );
}
} );
vus.sort( function ( a, b ) {
return ORDRE.indexOf( a ) - ORDRE.indexOf( b );
} );
return vus.length ? vus : [ '•' ];
}
function pastille( niveau, statut ) {
return '<span class="carte-osm-pastille carte-osm-' + NIVEAUX[ niveau ].famille +
( statut === 'privé' ? ' carte-osm-prive' : '' ) + '" aria-hidden="true">' + niveau + '</span>';
}
function pastillesDe( p ) {
return p.niveaux.map( function ( n ) {
return pastille( n, p.statut );
} ).join( '' );
}
function texteTitre( h ) {
var cible = h.querySelector( '.mw-headline' ) || h;
return cible.textContent.replace( /\s*\([^)]*\)\s*$/, '' ).trim();
}
/* Le nom d'un lieu : la page liée (titre complet), sinon le texte de la ligne. */
function lireNom( li, el ) {
var liens = li ? li.querySelectorAll( 'a' ) : [],
lien = null, i;
for ( i = 0; i < liens.length; i++ ) {
if ( !liens[ i ].closest( '.reference, .point-carte-voir, .mw-cite-backlink' ) ) {
lien = liens[ i ];
break;
}
}
if ( lien ) {
return {
court: lien.textContent.trim(),
complet: ( lien.getAttribute( 'title' ) || lien.textContent )
.replace( /\s*\(page inexistante\)$/, '' ).trim(),
lien: lien.getAttribute( 'href' ),
rouge: lien.classList.contains( 'new' )
};
}
var copie = ( li || el.parentNode ).cloneNode( true );
copie.querySelectorAll( 'sup, ul, ol, .point-carte-voir' ).forEach( function ( x ) {
x.remove();
} );
var texte = copie.textContent.replace( /\s*\(.*$/, '' ).trim();
return { court: texte, complet: texte, lien: null, rouge: false };
}
/* Sans lien, « Guy de Maupassant » devient « Lycée Guy de Maupassant ». */
function nomComplet( p ) {
if ( p.nom.lien || p.niveaux[ 0 ] === '•' ||
/^(École|Ecole|Collège|Lycée|Groupe|Institut|Cité)\b/i.test( p.nom.complet ) ) {
return p.nom.complet;
}
return NIVEAUX[ p.niveaux[ p.niveaux.length - 1 ] ].nom + ' ' + p.nom.complet;
}
/* ---------- lecture de la page ---------- */
function lirePoints( racine ) {
var points = [], sections = [], index = {}, courante = null;
racine.querySelectorAll( 'h2, h3, .point-carte' ).forEach( function ( el ) {
if ( !el.classList.contains( 'point-carte' ) ) {
courante = texteTitre( el );
return;
}
var lat = parseFloat( el.getAttribute( 'data-lat' ) ),
lon = parseFloat( el.getAttribute( 'data-lon' ) );
if ( isNaN( lat ) || isNaN( lon ) ) {
return;
}
var cle = courante || 'Autres lieux',
li = el.closest( 'li' );
if ( !index[ cle ] ) {
index[ cle ] = { nom: cle, points: [], actif: true };
sections.push( index[ cle ] );
}
var p = {
lat: lat,
lon: lon,
niveaux: lireNiveaux( el.getAttribute( 'data-niveau' ) ),
statut: /priv/i.test( el.getAttribute( 'data-statut' ) || '' ) ? 'privé' : 'public',
adresse: el.getAttribute( 'data-adresse' ) || '',
section: index[ cle ],
el: el
};
p.nom = lireNom( li, el );
index[ cle ].points.push( p );
points.push( p );
} );
return { points: points, sections: sections };
}
/* ---------- construction d'une carte ---------- */
function construire( conteneur, racine ) {
var donnees = lirePoints( racine );
if ( !donnees.points.length ) {
return;
}
conteneur.classList.add( 'carte-osm-prete' );
conteneur.innerHTML = '';
/* légende et filtres, au-dessus de la carte */
var legende = document.createElement( 'div' );
legende.className = 'carte-osm-legende';
legende.setAttribute( 'role', 'group' );
legende.setAttribute( 'aria-label', 'Lieux affichés sur la carte' );
donnees.sections.forEach( function ( s ) {
var etiquette = document.createElement( 'label' ),
caseACocher = document.createElement( 'input' );
caseACocher.type = 'checkbox';
caseACocher.checked = true;
caseACocher.setAttribute( 'aria-label', s.nom + ' (' + s.points.length + ')' );
caseACocher.addEventListener( 'change', function () {
s.actif = caseACocher.checked;
dessiner();
} );
s.caseACocher = caseACocher;
etiquette.appendChild( caseACocher );
etiquette.insertAdjacentHTML( 'beforeend',
'<span class="carte-osm-puces">' + pastillesDe( s.points[ 0 ] ) + '</span>' +
'<span>' + echapper( s.nom ) + '</span>' +
'<span class="carte-osm-compte">' + s.points.length + '</span>' );
legende.appendChild( etiquette );
} );
var fond = document.createElement( 'div' );
fond.className = 'carte-osm-fond';
fond.setAttribute( 'aria-label', conteneur.getAttribute( 'data-titre' ) || 'Carte' );
conteneur.appendChild( legende );
conteneur.appendChild( fond );
var carte = L.map( fond, {
zoomControl: false,
scrollWheelZoom: false, // la molette fait défiler la page tant qu'on n'a pas cliqué la carte
minZoom: 12,
maxZoom: 19,
zoomAnimation: !reduit,
fadeAnimation: !reduit,
markerZoomAnimation: !reduit
} );
L.tileLayer( TUILES, { maxZoom: 19, attribution: ATTRIBUTION } ).addTo( carte );
carte.attributionControl.setPrefix( '<a href="https://leafletjs.com/">Leaflet</a>' );
L.control.zoom( { zoomInTitle: 'Zoom avant', zoomOutTitle: 'Zoom arrière' } ).addTo( carte );
var limites = L.latLngBounds( donnees.points.map( function ( p ) {
return [ p.lat, p.lon ];
} ) );
function cadrer() {
carte.fitBounds( limites, { padding: [ 24, 24 ] } );
}
var ToutVoir = L.Control.extend( {
options: { position: 'topleft' },
onAdd: function () {
var bouton = L.DomUtil.create( 'button', 'carte-osm-toutvoir' );
bouton.type = 'button';
bouton.textContent = 'Tout voir';
bouton.title = 'Revenir à la vue d’ensemble';
L.DomEvent.disableClickPropagation( bouton );
L.DomEvent.on( bouton, 'click', cadrer );
return bouton;
}
} );
new ToutVoir().addTo( carte );
carte.on( 'focus click', function () {
carte.scrollWheelZoom.enable();
} );
carte.on( 'blur mouseout', function () {
carte.scrollWheelZoom.disable();
} );
/* ---------- regroupement des lieux trop proches à l'échelle affichée ---------- */
var calque = L.layerGroup().addTo( carte ),
groupes = [];
function rang( p ) {
return ORDRE.indexOf( p.niveaux[ 0 ] ) * 2 + ( p.statut === 'privé' ? 1 : 0 );
}
function bulle( g ) {
var html = '<div class="carte-osm-bulle"><ul>';
g.membres.slice().sort( function ( a, b ) {
return rang( a ) - rang( b ) || nomComplet( a ).localeCompare( nomComplet( b ), 'fr' );
} ).forEach( function ( p ) {
var nom = echapper( nomComplet( p ) );
if ( p.nom.lien ) {
nom = '<a href="' + echapper( p.nom.lien ) + '"' + ( p.nom.rouge ? ' class="new"' : '' ) + '>' + nom + '</a>';
}
html += '<li><span class="carte-osm-puces">' + pastillesDe( p ) + '</span><span>' +
'<span class="carte-osm-nom">' + nom + '</span>' +
'<span class="carte-osm-detail">Enseignement ' + p.statut +
( p.adresse ? ' · ' + echapper( p.adresse ) : '' ) + '</span></span></li>';
} );
html += '</ul>';
if ( g.etendu ) {
html += '<button type="button" class="carte-osm-zoom">Zoomer sur ces ' + g.membres.length + ' lieux</button>';
}
return html + '</div>';
}
/* Un groupe = les lieux qui se chevaucheraient à l'écran. Son marqueur aligne
une pastille par niveau présent, d'où une largeur variable : on compare
donc les boîtes réelles des marqueurs, pas seulement leurs centres, et on
fusionne tant qu'il reste un chevauchement. */
function majGroupe( g ) {
var x = 0, y = 0, vues = {};
g.membres.forEach( function ( p ) {
x += p.px.x / g.membres.length;
y += p.px.y / g.membres.length;
} );
g.px = L.point( x, y );
g.puces = [];
ORDRE.forEach( function ( n ) {
[ 'public', 'privé' ].forEach( function ( st ) {
g.membres.forEach( function ( p ) {
if ( p.statut === st && p.niveaux.indexOf( n ) !== -1 && !vues[ n + st ] ) {
vues[ n + st ] = true;
g.puces.push( pastille( n, st ) );
}
} );
} );
} );
g.largeur = g.puces.length * 24 + 2;
}
function chevauchent( a, b ) {
return Math.abs( a.px.x - b.px.x ) < ( a.largeur + b.largeur ) / 2 + MARGE_PX &&
Math.abs( a.px.y - b.px.y ) < HAUTEUR_PX + MARGE_PX;
}
function dessiner() {
calque.clearLayers();
groupes = donnees.points.filter( function ( p ) {
return p.section.actif;
} ).map( function ( p ) {
p.px = carte.latLngToLayerPoint( [ p.lat, p.lon ] );
var g = { membres: [ p ] };
majGroupe( g );
return g;
} );
var i, j, fusion = true;
while ( fusion ) {
fusion = false;
for ( i = 0; i < groupes.length && !fusion; i++ ) {
for ( j = i + 1; j < groupes.length; j++ ) {
if ( chevauchent( groupes[ i ], groupes[ j ] ) ) {
groupes[ i ].membres = groupes[ i ].membres.concat( groupes[ j ].membres );
majGroupe( groupes[ i ] );
groupes.splice( j, 1 );
fusion = true;
break;
}
}
}
}
groupes.forEach( function ( g ) {
g.limites = L.latLngBounds( g.membres.map( function ( p ) {
return [ p.lat, p.lon ];
} ) );
g.etendu = g.membres.length > 1 &&
carte.distance( g.limites.getSouthWest(), g.limites.getNorthEast() ) > 5;
var puces = g.puces,
noms = g.membres.map( nomComplet ).join( ', ' ),
html = '<span class="carte-osm-marque">' + puces.join( '' ) +
( g.membres.length > puces.length ? '<span class="carte-osm-nombre">' + g.membres.length + '</span>' : '' ) +
'</span>';
g.marque = L.marker( carte.layerPointToLatLng( g.px ), {
icon: L.divIcon( { className: 'carte-osm-icone', html: html, iconSize: null } ),
title: noms,
keyboard: true,
riseOnHover: true
} ).bindPopup( bulle( g ), { maxWidth: 300, minWidth: 200, maxHeight: 280 } ).addTo( calque );
var icone = g.marque.getElement();
if ( icone ) {
icone.setAttribute( 'aria-label', noms );
}
g.marque.on( 'popupopen', function ( e ) {
var fermer = e.popup.getElement().querySelector( '.leaflet-popup-close-button' ),
bouton = e.popup.getElement().querySelector( '.carte-osm-zoom' );
if ( fermer ) {
fermer.setAttribute( 'aria-label', 'Fermer' );
fermer.title = 'Fermer';
}
if ( bouton ) {
bouton.addEventListener( 'click', function () {
carte.closePopup();
carte.fitBounds( g.limites, { padding: [ 48, 48 ], maxZoom: 18 } );
} );
}
} );
} );
}
carte.on( 'zoomend', dessiner );
/* ---------- « situer sur la carte » depuis la liste ---------- */
donnees.points.forEach( function ( p ) {
if ( p.el.nextElementSibling && p.el.nextElementSibling.classList.contains( 'point-carte-voir' ) ) {
return;
}
var bouton = document.createElement( 'button' );
bouton.type = 'button';
bouton.className = 'point-carte-voir';
bouton.textContent = '📍';
bouton.title = 'Situer sur la carte';
bouton.setAttribute( 'aria-label', 'Situer ' + nomComplet( p ) + ' sur la carte' );
bouton.addEventListener( 'click', function () {
if ( !p.section.actif ) {
p.section.actif = true;
p.section.caseACocher.checked = true;
}
conteneur.scrollIntoView( { behavior: reduit ? 'auto' : 'smooth', block: 'center' } );
var cible = L.latLng( p.lat, p.lon ), zoom = Math.max( carte.getZoom(), 17 );
function ouvrir() {
dessiner();
groupes.forEach( function ( g ) {
if ( g.membres.indexOf( p ) !== -1 ) {
g.marque.openPopup();
}
} );
}
if ( carte.getZoom() === zoom && carte.getCenter().distanceTo( cible ) < 1 ) {
ouvrir();
} else {
carte.once( 'moveend', ouvrir );
carte.setView( cible, zoom, { animate: !reduit } );
}
} );
p.el.insertAdjacentElement( 'afterend', bouton );
} );
/* une carte dans une section repliée (vue mobile) naît sans dimensions */
function montrer() {
carte.invalidateSize();
cadrer();
dessiner();
}
if ( fond.offsetWidth && fond.offsetHeight ) {
montrer();
}
if ( window.ResizeObserver ) {
var avant = fond.offsetWidth;
new ResizeObserver( function () {
if ( fond.offsetWidth && !avant ) {
montrer();
} else if ( fond.offsetWidth ) {
carte.invalidateSize();
}
avant = fond.offsetWidth;
} ).observe( fond );
}
}
/* ---------- chargement de Leaflet, puis des cartes de la page ---------- */
var leaflet = null;
function chargerLeaflet() {
if ( !leaflet ) {
mw.loader.load( LEAFLET + 'leaflet.css', 'text/css' );
leaflet = window.L ? Promise.resolve() : mw.loader.getScript( LEAFLET + 'leaflet.js' );
}
return leaflet;
}
mw.hook( 'wikipage.content' ).add( function ( $contenu ) {
var racine = $contenu && $contenu[ 0 ],
cartes = racine ? racine.querySelectorAll( '.carte-osm:not(.carte-osm-prete)' ) : [];
if ( !cartes.length ) {
return;
}
chargerLeaflet().then( function () {
cartes.forEach( function ( conteneur ) {
construire( conteneur, racine );
} );
}, function () {
cartes.forEach( function ( conteneur ) {
var repli = conteneur.querySelector( '.carte-osm-repli' );
if ( repli ) {
repli.textContent = 'La carte n’a pas pu être chargée.';
}
} );
} );
} );
}() );