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.
 *
 * Côté wikicode :
 *   - {{Carte OSM}} place une carte dans la page ;
 *   - {{Point carte|latitude|longitude|…}}, posé à côté d'un nom, dans une liste
 *     ou dans la première colonne d'un tableau, y ajoute ce lieu.
 *
 * Deux sortes de cartes :
 *   - la carte d'un article montre les points de cet article ; sa légende reprend
 *     les titres de section (niveau 3, à défaut 2) qui les précèdent ;
 *   - une carte d'ensemble ({{Carte OSM|sources=Article 1; Article 2}}) ne contient
 *     aucun point : elle lit ceux des articles cités, à chaque affichage. Rien n'est
 *     recopié, donc rien n'est à synchroniser ; sa légende propose un thème par
 *     article, nommé par le paramètre « thème » de la carte de cet article.
 *
 * Pastilles : pour un établissement scolaire (niveau=…), une lettre par niveau —
 * M maternelle, É élémentaire, P primaire en bleu, C collège en orange, L lycée en
 * vert ; pour tout autre lieu, le pictogramme du lieu (symbole=…) ou, à défaut,
 * celui de son thème. Carré : établissement privé ou lieu associatif.
 *
 * Les utilisateurs connectés peuvent déplacer un lieu depuis sa fiche (bouton
 * « Déplacer ») : le script réécrit alors les coordonnées de son {{Point carte}}
 * dans l'article où il est défini, même depuis une carte d'ensemble.
 *
 * 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 = '&copy; <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' }
	};
	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;
	var compteur = 0;           // identifiant unique de chaque point, toutes cartes confondues

	/* ---------- utilitaires ---------- */

	function echapper( s ) {
		return String( s ).replace( /[&<>"']/g, function ( c ) {
			return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[ c ];
		} );
	}

	function simplifier( s ) {
		return String( s ).toLowerCase().normalize( 'NFD' ).replace( /[̀-ͯ]/g, '' ).replace( /[^a-z0-9]+/g, ' ' ).trim();
	}

	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 );
			}
		} );
		return vus.sort( function ( a, b ) {
			return ORDRE.indexOf( a ) - ORDRE.indexOf( b );
		} );
	}

	/* Les pastilles d'un point : lettres de niveau pour une école, sinon un pictogramme. */
	function pucesDe( p ) {
		var carre = p.statut !== 'public';
		if ( p.niveaux.length ) {
			return p.niveaux.map( function ( n ) {
				return { texte: n, classe: 'carte-osm-' + NIVEAUX[ n ].famille, carre: carre, rang: ORDRE.indexOf( n ) };
			} );
		}
		return [ { texte: p.symbole || '•', classe: 'carte-osm-symbole', carre: carre, rang: 10 } ];
	}

	function htmlPuce( u ) {
		return '<span class="carte-osm-pastille ' + u.classe + ( u.carre ? ' carte-osm-prive' : '' ) +
			'" aria-hidden="true">' + echapper( u.texte ) + '</span>';
	}

	/* Réunit des pastilles sans doublon, dans un ordre stable. */
	function fusionnerPuces( listes, ignorerForme ) {
		var vues = {}, res = [];
		listes.forEach( function ( liste ) {
			liste.forEach( function ( u ) {
				var cle = u.texte + '|' + u.classe + ( ignorerForme ? '' : '|' + u.carre );
				if ( !vues[ cle ] ) {
					vues[ cle ] = true;
					res.push( u );
				}
			} );
		} );
		return res.sort( function ( a, b ) {
			return a.rang - b.rang || ( a.carre === b.carre ? 0 : a.carre ? 1 : -1 );
		} );
	}

	function pastillesDe( p ) {
		return pucesDe( p ).map( htmlPuce ).join( '' );
	}

	function texteTitre( h ) {
		var cible = h.querySelector( '.mw-headline' ) || h;
		return cible.textContent.replace( /\s*\([^)]*\)\s*$/, '' ).trim();
	}

	function lienValable( a ) {
		return !a.closest( '.reference, .point-carte-voir, .mw-cite-backlink, sup' ) &&
			!a.classList.contains( 'external' ) && !a.classList.contains( 'mw-file-description' );
	}

	/* Le nom d'un lieu : le lien le plus proche qui le précède dans la même cellule
	   (ou le même élément de liste) ; à défaut, le premier lien de la ligne ; à défaut,
	   le texte. Du lien, on garde le titre de la page quand il contient le texte
	   affiché (« Ambroise Paré » → « École maternelle Ambroise Paré »), sinon le texte
	   (« Médiathèque de la Marine », lien vers « Médiathèques de Colombes »). */
	function lireNom( el ) {
		var force = ( el.getAttribute( 'data-nom' ) || '' ).trim(),
			cellule = el.closest( 'td, th, li' ) || el.parentNode,
			ligne = el.closest( 'tr, li' ) || cellule,
			lien = null;
		Array.prototype.forEach.call( cellule.querySelectorAll( 'a' ), function ( a ) {
			if ( lienValable( a ) && ( a.compareDocumentPosition( el ) & Node.DOCUMENT_POSITION_FOLLOWING ) ) {
				lien = a;
			}
		} );
		if ( !lien ) {
			lien = Array.prototype.filter.call( ligne.querySelectorAll( 'a' ), lienValable )[ 0 ] || null;
		}
		if ( lien ) {
			var texte = lien.textContent.trim(),
				titre = ( lien.getAttribute( 'title' ) || texte ).replace( /\s*\(page inexistante\)$/, '' ).trim();
			return {
				complet: force || ( simplifier( titre ).indexOf( simplifier( texte ) ) !== -1 ? titre : texte ),
				lien: lien.getAttribute( 'href' ),
				rouge: lien.classList.contains( 'new' ),
				force: !!force
			};
		}
		var copie = cellule.cloneNode( true );
		copie.querySelectorAll( 'sup, ul, ol, .point-carte-voir' ).forEach( function ( x ) {
			x.remove();
		} );
		var brut = copie.textContent.replace( /\s*\(.*$/, '' ).trim();
		return { complet: force || brut, lien: null, rouge: false, force: !!force };
	}

	/* Sans lien, « Guy de Maupassant » devient « Lycée Guy de Maupassant ». */
	function nomComplet( p ) {
		if ( p.nom.lien || p.nom.force || !p.niveaux.length ||
			/^(É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;
	}

	function detail( p, ensemble ) {
		var morceaux = [];
		if ( ensemble && p.theme ) {
			morceaux.push( p.theme.nom );
		}
		if ( p.niveaux.length ) {
			morceaux.push( 'Enseignement ' + p.statut );
		} else if ( p.statut === 'associatif' ) {
			morceaux.push( 'Lieu associatif' );
		} else if ( p.statut === 'privé' ) {
			morceaux.push( 'Lieu privé' );
		}
		if ( p.adresse ) {
			morceaux.push( p.adresse );
		}
		return morceaux.map( echapper ).join( ' · ' );
	}

	/* ---------- lecture des points d'une page ---------- */

	/* racine : le contenu d'une page (celle qu'on lit, ou une page source chargée par
	   l'API) ; source : son titre, ou null pour la page affichée. Le rang d'un point
	   est son numéro d'ordre parmi les {{Point carte}} de sa page : c'est lui qui
	   désigne le modèle à réécrire quand on déplace le lieu. */
	function lirePoints( racine, source, theme ) {
		var points = [], sections = [], index = {}, courante = null, rang = 0;
		racine.querySelectorAll( 'h2, h3, .point-carte' ).forEach( function ( el ) {
			if ( !el.classList.contains( 'point-carte' ) ) {
				courante = texteTitre( el );
				return;
			}
			var rangIci = rang++,
				lat = parseFloat( el.getAttribute( 'data-lat' ) ),
				lon = parseFloat( el.getAttribute( 'data-lon' ) );
			if ( isNaN( lat ) || isNaN( lon ) ) {
				return;
			}
			var statut = ( el.getAttribute( 'data-statut' ) || '' ).toLowerCase(),
				cle = courante || 'Autres lieux';
			if ( !index[ cle ] ) {
				index[ cle ] = { nom: cle, points: [], actif: true };
				sections.push( index[ cle ] );
			}
			var p = {
				id: compteur++,
				lat: lat,
				lon: lon,
				niveaux: lireNiveaux( el.getAttribute( 'data-niveau' ) ),
				statut: /priv/.test( statut ) ? 'privé' : /assoc/.test( statut ) ? 'associatif' : 'public',
				adresse: el.getAttribute( 'data-adresse' ) || '',
				symbole: el.getAttribute( 'data-symbole' ) || ( theme && theme.symbole ) || '',
				section: index[ cle ],
				theme: theme,
				source: source,
				el: el,
				rang: rangIci
			};
			p.nom = lireNom( el );
			index[ cle ].points.push( p );
			points.push( p );
		} );
		return { points: points, sections: sections };
	}

	function themeDe( racine, titre ) {
		var c = racine.querySelector( '.carte-osm' );
		return {
			nom: ( c && c.getAttribute( 'data-theme' ) ) || titre,
			symbole: ( c && c.getAttribute( 'data-symbole' ) ) || '',
			page: titre,
			points: [],
			actif: true
		};
	}

	/* Charge une page source par l'API et en lit les points. */
	function chargerSource( titre ) {
		return mw.loader.using( 'mediawiki.api' ).then( function () {
			return new mw.Api().get( {
				action: 'parse',
				page: titre,
				prop: 'text',
				redirects: 1,
				disableeditsection: 1,
				disablelimitreport: 1,
				formatversion: 2
			} );
		} ).then( function ( r ) {
			var doc = new DOMParser().parseFromString( r.parse.text, 'text/html' ),
				theme = themeDe( doc, r.parse.title ),
				lus = lirePoints( doc, r.parse.title, theme );
			theme.points = lus.points;
			return theme;
		}, function () {
			return { nom: titre, page: titre, points: [], actif: true, erreur: true };
		} );
	}

	/* ---------- construction d'une carte ---------- */

	function construire( conteneur, racine ) {
		var sources = ( conteneur.getAttribute( 'data-sources' ) || '' ).split( /\s*;\s*/ ).map( function ( s ) {
			return s.trim();
		} ).filter( Boolean );

		if ( !sources.length ) {
			var themeLocal = themeDe( racine, mw.config.get( 'wgTitle' ) ),
				lus = lirePoints( racine, null, themeLocal );
			if ( lus.points.length ) {
				dessinerCarte( conteneur, lus.points, lus.sections, false, [] );
			}
			return;
		}
		conteneur.classList.add( 'carte-osm-prete' );
		$.when.apply( $, sources.map( chargerSource ) ).then( function () {
			var themes = Array.prototype.slice.call( arguments ),
				points = [], manquants = [];
			themes.forEach( function ( t ) {
				if ( t.erreur || !t.points.length ) {
					manquants.push( t.page );
				}
				points = points.concat( t.points );
				t.points.forEach( function ( p ) {
					p.section = t;       // dans une carte d'ensemble, la légende filtre par thème
				} );
			} );
			themes = themes.filter( function ( t ) {
				return t.points.length;
			} );
			if ( points.length ) {
				dessinerCarte( conteneur, points, themes, true, manquants );
			} else {
				var repli = conteneur.querySelector( '.carte-osm-repli' );
				if ( repli ) {
					repli.textContent = 'Aucun lieu trouvé dans les articles sources.';
				}
			}
		} );
	}

	/* points : les lieux ; filtres : les entrées de la légende (sections d'un article,
	   ou thèmes d'une carte d'ensemble) ; ensemble : vrai pour une carte d'ensemble. */
	function dessinerCarte( conteneur, points, filtres, ensemble, manquants ) {
		conteneur.classList.add( 'carte-osm-prete' );
		conteneur.innerHTML = '';

		var cfg = mw.config.get( [ 'wgUserName', 'wgIsProbablyEditable', 'wgAction', 'wgRevisionId', 'wgCurRevisionId', 'wgPageName', 'wgScript' ] ),
			peutDeplacer = !!cfg.wgUserName && !!cfg.wgIsProbablyEditable && cfg.wgAction === 'view' &&
				!!cfg.wgRevisionId && cfg.wgRevisionId === cfg.wgCurRevisionId;

		/* 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' );
		filtres.forEach( function ( s ) {
			var etiquette = document.createElement( 'label' ),
				caseACocher = document.createElement( 'input' ),
				echantillon = ensemble ?
					fusionnerPuces( s.points.map( pucesDe ), true ).slice( 0, 5 ) :
					pucesDe( s.points[ 0 ] );
			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">' + echantillon.map( htmlPuce ).join( '' ) + '</span>' +
				'<span>' + echapper( s.nom ) + '</span>' +
				'<span class="carte-osm-compte">' + s.points.length + '</span>' );
			legende.appendChild( etiquette );
		} );
		if ( ensemble && points.some( function ( p ) {
			return p.statut !== 'public';
		} ) ) {
			legende.insertAdjacentHTML( 'beforeend', '<span class="carte-osm-note">' +
				'<span class="carte-osm-pastille carte-osm-symbole carte-osm-prive" aria-hidden="true"></span>' +
				'carré : privé ou associatif</span>' );
		}
		if ( manquants.length ) {
			legende.insertAdjacentHTML( 'beforeend', '<span class="carte-osm-note">Sans lieu ou introuvable : ' +
				manquants.map( echapper ).join( ', ' ) + '</span>' );
		}

		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;
		function calculerLimites() {
			limites = L.latLngBounds( points.map( function ( q ) {
				return [ q.lat, q.lon ];
			} ) );
		}
		calculerLimites();
		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 ordre( p ) {
			return ( p.niveaux.length ? ORDRE.indexOf( p.niveaux[ 0 ] ) : 10 ) * 2 + ( p.statut === 'public' ? 0 : 1 );
		}

		function bulle( g ) {
			var html = '<div class="carte-osm-bulle"><ul>';
			g.membres.slice().sort( function ( a, b ) {
				return ordre( a ) - ordre( b ) || nomComplet( a ).localeCompare( nomComplet( b ), 'fr' );
			} ).forEach( function ( p ) {
				var nom = echapper( nomComplet( p ) ), ligne = detail( p, ensemble );
				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>' +
					( ligne ? '<span class="carte-osm-detail">' + ligne + '</span>' : '' ) +
					( peutDeplacer ? '<button type="button" class="carte-osm-deplacer" data-id="' + p.id + '">Déplacer</button>' : '' ) +
					'</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
		   leurs pastilles, 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;
			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 = fusionnerPuces( g.membres.map( pucesDe ), false );
			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 = 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 noms = g.membres.map( nomComplet ).join( ', ' ),
					html = '<span class="carte-osm-marque">' + g.puces.map( htmlPuce ).join( '' ) +
						( g.membres.length > g.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 element = e.popup.getElement(),
						fermer = element.querySelector( '.leaflet-popup-close-button' ),
						bouton = element.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 } );
						} );
					}
					element.querySelectorAll( '.carte-osm-deplacer' ).forEach( function ( b ) {
						b.addEventListener( 'click', function () {
							var vise = Number( b.getAttribute( 'data-id' ) );
							g.membres.forEach( function ( p ) {
								if ( p.id === vise ) {
									commencerDeplacement( p );
								}
							} );
						} );
					} );
				} );
			} );
		}

		carte.on( 'zoomend', dessiner );

		/* ---------- déplacer un lieu (utilisateurs connectés) ----------
		   « Déplacer » dans la fiche d'un lieu, puis un clic sur la carte à l'endroit
		   voulu ; le repère provisoire s'ajuste à la souris avant d'enregistrer.
		   L'enregistrement réécrit les coordonnées du n-ième {{Point carte}} de
		   l'article où le lieu est défini — la page affichée, ou l'article source
		   d'une carte d'ensemble : c'est une modification ordinaire de cet article
		   (historique, résumé, annulation possible). Si l'article a changé depuis son
		   affichage, rien n'est écrit. */

		var bandeau = null, enCours = null;

		function afficherBandeau( html ) {
			if ( !bandeau ) {
				bandeau = L.DomUtil.create( 'div', 'carte-osm-bandeau', fond );
				bandeau.setAttribute( 'role', 'status' );
				L.DomEvent.disableClickPropagation( bandeau );
				L.DomEvent.disableScrollPropagation( bandeau );
			}
			bandeau.innerHTML = html;
			bandeau.hidden = false;
			var fermer = bandeau.querySelector( '.carte-osm-annuler' );
			if ( fermer ) {
				fermer.addEventListener( 'click', annuler );
			}
			return bandeau;
		}

		function arreterDeplacement() {
			if ( enCours && enCours.repere ) {
				carte.removeLayer( enCours.repere );
			}
			carte.off( 'click', viser );
			fond.classList.remove( 'carte-osm-visee' );
			enCours = null;
		}

		function annuler() {
			arreterDeplacement();
			if ( bandeau ) {
				bandeau.hidden = true;
			}
		}

		function commencerDeplacement( p ) {
			arreterDeplacement();
			carte.closePopup();
			enCours = { p: p, repere: null };
			fond.classList.add( 'carte-osm-visee' );
			afficherBandeau( 'Cliquez sur la carte à l’emplacement de <b>' + echapper( nomComplet( p ) ) + '</b>. ' +
				'<button type="button" class="carte-osm-annuler">Annuler</button>' );
			carte.on( 'click', viser );
		}

		function viser( e ) {
			if ( !enCours ) {
				return;
			}
			carte.off( 'click', viser );
			fond.classList.remove( 'carte-osm-visee' );
			var p = enCours.p;
			enCours.repere = L.marker( e.latlng, {
				draggable: true,
				zIndexOffset: 1000,
				title: 'Nouvelle position : ' + nomComplet( p ),
				icon: L.divIcon( {
					className: 'carte-osm-icone',
					html: '<span class="carte-osm-marque carte-osm-nouvelle">' + pastillesDe( p ) + '</span>',
					iconSize: null
				} )
			} ).addTo( carte );
			afficherBandeau( 'Nouvelle position de <b>' + echapper( nomComplet( p ) ) + '</b>&nbsp;: ' +
				'<span class="carte-osm-coord"></span> ' +
				'<span class="carte-osm-aide">— faites glisser le repère pour l’ajuster.</span> ' +
				'<button type="button" class="carte-osm-enregistrer">Enregistrer</button> ' +
				'<button type="button" class="carte-osm-annuler">Annuler</button>' );
			function coord() {
				var ll = enCours.repere.getLatLng();
				bandeau.querySelector( '.carte-osm-coord' ).textContent = ll.lat.toFixed( 6 ) + ', ' + ll.lng.toFixed( 6 );
			}
			coord();
			enCours.repere.on( 'drag dragend', coord );
			bandeau.querySelector( '.carte-osm-enregistrer' ).addEventListener( 'click', enregistrer );
		}

		function enregistrer() {
			var p = enCours.p,
				page = p.source || cfg.wgPageName,
				ll = enCours.repere.getLatLng(),
				lat = ll.lat.toFixed( 6 ),
				lon = ll.lng.toFixed( 6 ),
				ancienLat = parseFloat( p.el.getAttribute( 'data-lat' ) ),
				ancienLon = parseFloat( p.el.getAttribute( 'data-lon' ) );
			enCours.repere.dragging.disable();
			afficherBandeau( 'Enregistrement…' );
			mw.loader.using( 'mediawiki.api' ).then( function () {
				return new mw.Api().edit( page, function ( revision ) {
					var n = 0, ok = false;
					var texte = revision.content.replace( /(\{\{\s*[Pp]oint[ _]carte\s*\|)([^|}]*)\|([^|}]*)/g,
						function ( tout, debut, la, lo ) {
							if ( n++ !== p.rang ) {
								return tout;
							}
							// garde-fou : le modèle visé doit porter les coordonnées affichées
							if ( Math.abs( parseFloat( la ) - ancienLat ) > 1e-7 || Math.abs( parseFloat( lo ) - ancienLon ) > 1e-7 ) {
								return tout;
							}
							ok = true;
							return debut + lat + '|' + lon;
						} );
					if ( !ok ) {
						return $.Deferred().reject( 'carte-decalee' ).promise();
					}
					return {
						text: texte,
						summary: 'Carte : nouvelle position pour ' + nomComplet( p ) + ' (' + lat + ', ' + lon + ')',
						minor: true
					};
				} );
			} ).then( function () {
				p.lat = parseFloat( lat );
				p.lon = parseFloat( lon );
				p.el.setAttribute( 'data-lat', lat );
				p.el.setAttribute( 'data-lon', lon );
				calculerLimites();
				arreterDeplacement();
				dessiner();
				afficherBandeau( '✓ Position de <b>' + echapper( nomComplet( p ) ) + '</b> enregistrée' +
					( p.source ? ' dans « ' + echapper( p.source ) + ' »' : '' ) + '. ' +
					'<a href="' + echapper( cfg.wgScript + '?title=' + encodeURIComponent( page ) + '&action=history' ) + '">Historique</a> ' +
					'<button type="button" class="carte-osm-annuler">Fermer</button>' );
			}, function ( code ) {
				var message = code === 'carte-decalee' ?
					'L’article a changé depuis son affichage : rechargez la page, puis recommencez.' :
					'L’enregistrement a échoué (' + echapper( code ) + '). Les coordonnées peuvent aussi se corriger dans le wikicode.';
				if ( enCours && enCours.repere ) {
					enCours.repere.dragging.enable();
				}
				afficherBandeau( message + ' <button type="button" class="carte-osm-annuler">Fermer</button>' );
			} );
		}

		if ( peutDeplacer ) {
			document.addEventListener( 'keydown', function ( e ) {
				if ( e.key === 'Escape' && enCours ) {
					annuler();
				}
			} );
		}

		/* ---------- « situer sur la carte » depuis la page (points de la page seulement) ---------- */

		points.forEach( function ( p ) {
			if ( p.source || ( 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.';
				}
			} );
		} );
	} );
}() );