MediaWiki:Gadget-WikiMap.js: Unterschied zwischen den Versionen
Admin (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung |
Admin (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung |
||
| Zeile 30: | Zeile 30: | ||
( function () { | ( function () { | ||
'use strict'; | 'use strict'; | ||
/* ==================================================================== | /* ==================================================================== | ||
* 1) KONFIGURATION | * 1) KONFIGURATION | ||
| Zeile 38: | Zeile 38: | ||
gemeindenSeite: 'MediaWiki:WikiMap-Gemeinden.json', | gemeindenSeite: 'MediaWiki:WikiMap-Gemeinden.json', | ||
defaultCenter: [ 47.5, 13.5 ], | defaultCenter: [ 47.5, 13.5 ], | ||
defaultZoom: 7, | |||
focusZoom: 14 | |||
}; | }; | ||
var LEAFLET_MODUS = 'lokal'; // 'cdn' zum Testen, 'lokal' für Produktion | var LEAFLET_MODUS = 'lokal'; // 'cdn' zum Testen, 'lokal' für Produktion | ||
var LEAFLET_LOKAL_BASIS = '/resources/lib/leaflet/'; | var LEAFLET_LOKAL_BASIS = '/resources/lib/leaflet/'; | ||
var LEAFLET_QUELLEN = { | var LEAFLET_QUELLEN = { | ||
cdn: { | cdn: { | ||
| Zeile 67: | Zeile 67: | ||
} | } | ||
}; | }; | ||
/* ==================================================================== | /* ==================================================================== | ||
* 2) HINTERGRUNDKARTEN | * 2) HINTERGRUNDKARTEN | ||
| Zeile 75: | Zeile 75: | ||
* ==================================================================== */ | * ==================================================================== */ | ||
function baueLayer() { | function baueLayer() { | ||
var osmStandard = L.tileLayer( | var osmStandard = L.tileLayer( | ||
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', | 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', | ||
{ maxZoom: 19, attribution: '© OpenStreetMap-Mitwirkende' } | { maxZoom: 19, attribution: '© OpenStreetMap-Mitwirkende' } | ||
); | ); | ||
var osmRelief = L.tileLayer( | |||
'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', | |||
{ | |||
maxZoom: 17, | |||
attribution: 'Kartendaten: © OpenStreetMap-Mitwirkende, SRTM | ' + | |||
'Darstellung: © OpenTopoMap (CC-BY-SA)' | |||
} | |||
); | |||
var luftbild = L.tileLayer( | var luftbild = L.tileLayer( | ||
'https://mapsneu.wien.gv.at/basemap/bmaporthofoto30cm/' + | 'https://mapsneu.wien.gv.at/basemap/bmaporthofoto30cm/' + | ||
| Zeile 106: | Zeile 106: | ||
return { | return { | ||
basis: { | basis: { | ||
' | 'Luftbild + Karte': gemischt, | ||
'Luftbild': luftbild, | |||
'Karte': osmStandard, | |||
'TopoKarte': tracestrack | |||
}, | }, | ||
standard: gemischt | |||
}; | }; | ||
} | } | ||
/* ==================================================================== | /* ==================================================================== | ||
* 3) PIN-ICONS | * 3) PIN-ICONS | ||
| Zeile 121: | Zeile 121: | ||
return L.divIcon( { | return L.divIcon( { | ||
className: 'wikimap-pin' + ( aktiv ? ' wikimap-pin-aktiv' : '' ), | className: 'wikimap-pin' + ( aktiv ? ' wikimap-pin-aktiv' : '' ), | ||
iconSize: aktiv ? [ 28, 28 ] : [ 18, 18 ], | |||
iconAnchor: aktiv ? [ 14, 28 ] : [ 9, 18 ], | |||
popupAnchor: [ 0, aktiv ? -28 : -18 ] | |||
} ); | } ); | ||
} | } | ||
/* ==================================================================== | /* ==================================================================== | ||
* 4) DATEN AUS WIKI-JSON-SEITE | * 4) DATEN AUS WIKI-JSON-SEITE | ||
| Zeile 147: | Zeile 147: | ||
} ); | } ); | ||
} | } | ||
/* ==================================================================== | /* ==================================================================== | ||
* 5) EINE EINZELNE KARTE BAUEN | * 5) EINE EINZELNE KARTE BAUEN | ||
| Zeile 158: | Zeile 158: | ||
container.style.height = parseInt( hoehe, 10 ) + 'px'; | container.style.height = parseInt( hoehe, 10 ) + 'px'; | ||
} | } | ||
var hatFullscreen = !!( L.Control && L.Control.FullScreen ); | var hatFullscreen = !!( L.Control && L.Control.FullScreen ); | ||
var layer = baueLayer(); | var layer = baueLayer(); | ||
var map = L.map( container, { | var map = L.map( container, { | ||
| Zeile 169: | Zeile 169: | ||
} ); | } ); | ||
L.control.layers( layer.basis ).addTo( map ); | L.control.layers( layer.basis ).addTo( map ); | ||
// Sicherheitsnetz: Knopf explizit hinzufügen, falls die Option oben | // Sicherheitsnetz: Knopf explizit hinzufügen, falls die Option oben | ||
// nicht gegriffen hat (z.B. Plugin kam minimal verspätet). | // nicht gegriffen hat (z.B. Plugin kam minimal verspätet). | ||
| Zeile 175: | Zeile 175: | ||
map.addControl( new L.Control.FullScreen() ); | map.addControl( new L.Control.FullScreen() ); | ||
} | } | ||
if ( container.getAttribute( 'data-gemeinden' ) === 'ja' && gemeinden ) { | if ( container.getAttribute( 'data-gemeinden' ) === 'ja' && gemeinden ) { | ||
L.geoJSON( gemeinden, { | L.geoJSON( gemeinden, { | ||
| Zeile 196: | Zeile 196: | ||
} ).addTo( map ); | } ).addTo( map ); | ||
} | } | ||
var aktiverMarker = null; | var aktiverMarker = null; | ||
var sichtbare = []; | var sichtbare = []; | ||
| Zeile 209: | Zeile 209: | ||
var m = L.marker( [ ort.lat, ort.lon ], { | var m = L.marker( [ ort.lat, ort.lon ], { | ||
icon: icon( istAktiv ), | icon: icon( istAktiv ), | ||
zIndexOffset: istAktiv ? 1000 : 0 | |||
} ); | } ); | ||
var link = mw.util.getUrl( ort.seite || ort.name ); | var link = mw.util.getUrl( ort.seite || ort.name ); | ||
| Zeile 222: | Zeile 222: | ||
} | } | ||
} ); | } ); | ||
if ( aktiverMarker ) { | if ( aktiverMarker ) { | ||
map.setView( aktiverMarker.getLatLng(), CONFIG.focusZoom ); | map.setView( aktiverMarker.getLatLng(), CONFIG.focusZoom ); | ||
| Zeile 229: | Zeile 229: | ||
map.fitBounds( sichtbare, { padding: [ 30, 30 ] } ); | map.fitBounds( sichtbare, { padding: [ 30, 30 ] } ); | ||
} | } | ||
map.on( 'resize fullscreenchange', function () { | map.on( 'resize fullscreenchange', function () { | ||
setTimeout( function () { | setTimeout( function () { | ||
| Zeile 237: | Zeile 237: | ||
return map; | return map; | ||
} | } | ||
/* ==================================================================== | /* ==================================================================== | ||
* 6) EINSTIEG | * 6) EINSTIEG | ||
| Zeile 255: | Zeile 255: | ||
aufgaben.push( | aufgaben.push( | ||
brauchtGemeinden ? | brauchtGemeinden ? | ||
ladeJson( CONFIG.gemeindenSeite ).catch( function () { | |||
return null; | |||
} ) : | |||
$.Deferred().resolve( null ) | |||
); | ); | ||
$.when.apply( $, aufgaben ).then( function ( orteData, gemeinden ) { | $.when.apply( $, aufgaben ).then( function ( orteData, gemeinden ) { | ||
| Zeile 277: | Zeile 277: | ||
} ); | } ); | ||
} | } | ||
/* ==================================================================== | /* ==================================================================== | ||
* 7) LEAFLET SICHERSTELLEN, DANN STARTEN | * 7) LEAFLET SICHERSTELLEN, DANN STARTEN | ||
* ==================================================================== */ | * ==================================================================== */ | ||
// Lädt EIN Skript nach. Wichtig: manche Leaflet-Plugins sind UMD-Module | // Lädt EIN Skript nach. Wichtig: manche Leaflet-Plugins sind UMD-Module | ||
// und registrieren sich bei vorhandenem AMD-Loader (z.B. ResourceLoader) | // und registrieren sich bei vorhandenem AMD-Loader (z.B. ResourceLoader) | ||
| Zeile 311: | Zeile 311: | ||
} ).promise(); | } ).promise(); | ||
} | } | ||
function ladeCss( href ) { | function ladeCss( href ) { | ||
var l = document.createElement( 'link' ); | var l = document.createElement( 'link' ); | ||
| Zeile 318: | Zeile 318: | ||
document.head.appendChild( l ); | document.head.appendChild( l ); | ||
} | } | ||
function stelleLeafletBereit( fertig ) { | function stelleLeafletBereit( fertig ) { | ||
var q = LEAFLET_QUELLEN[ LEAFLET_MODUS ]; | var q = LEAFLET_QUELLEN[ LEAFLET_MODUS ]; | ||
// CSS immer einhängen (auch wenn L schon da ist, könnte Plugin-CSS fehlen). | // CSS immer einhängen (auch wenn L schon da ist, könnte Plugin-CSS fehlen). | ||
q.css.forEach( ladeCss ); | q.css.forEach( ladeCss ); | ||
// JS-Dateien strikt nacheinander laden und ERST starten, wenn sowohl | // JS-Dateien strikt nacheinander laden und ERST starten, wenn sowohl | ||
// L.map als auch L.Control.FullScreen existieren. | // L.map als auch L.Control.FullScreen existieren. | ||
| Zeile 337: | Zeile 337: | ||
} ); | } ); | ||
} ); | } ); | ||
kette.then( function () { | kette.then( function () { | ||
// Sicherheitsnetz: kurz warten, falls Plugin minimal verzögert ist. | // Sicherheitsnetz: kurz warten, falls Plugin minimal verzögert ist. | ||
| Zeile 352: | Zeile 352: | ||
} ); | } ); | ||
} | } | ||
mw.loader.using( [ 'mediawiki.api', 'mediawiki.util' ] ).then( function () { | mw.loader.using( [ 'mediawiki.api', 'mediawiki.util' ] ).then( function () { | ||
$( function () { | $( function () { | ||
| Zeile 361: | Zeile 361: | ||
} ); | } ); | ||
} ); | } ); | ||
}() ); | }() ); | ||
Version vom 9. Juni 2026, 20:37 Uhr
/**
* Gadget-WikiMap.js
* ============================================================================
* Zentrale Leaflet-Karten-Engine für das Wiki.
*
* KONZEPT
* -------
* EINE zentrale Datenquelle (MediaWiki:WikiMap-Orte.json) hält ALLE Orte.
* Jede Wiki-Seite bindet nur einen leeren <div class="wikimap"> ein und sagt
* über data-Attribute, welcher Ort gerade "aktiv" ist. Dieses Gadget zeichnet
* dann die Karte, alle Pins, hebt den aktiven Pin hervor und zentriert ihn.
*
* Verschiebst du einen Ort, änderst du EINE Zeile in der zentralen JSON –
* jede Karte im ganzen Wiki ist sofort aktuell.
*
* EINBINDUNG AUF EINER SEITE
* --------------------------
* <div class="wikimap" data-aktiv-ort="gaestehaus-mustertal"></div>
*
* data-Attribute:
* data-aktiv-ort ID des hervorgehobenen Ortes (rot, zentriert, Popup auf)
* data-filter nur Orte dieser "kategorie" zeigen
* data-gemeinden "ja" => Gemeinde-Umrisse einblenden (Klick => Seite)
* data-hoehe Pixel-Höhe der Karte (optional, sonst CSS-Default)
*
* ABHÄNGIGKEIT: Leaflet 1.9.x + Leaflet.fullscreen. Dieses Gadget lädt sie
* bei Bedarf selbst (siehe LEAFLET_QUELLEN unten).
* ============================================================================
*/
( function () {
'use strict';
/* ====================================================================
* 1) KONFIGURATION
* ==================================================================== */
var CONFIG = {
orteSeite: 'MediaWiki:WikiMap-Orte.json',
gemeindenSeite: 'MediaWiki:WikiMap-Gemeinden.json',
defaultCenter: [ 47.5, 13.5 ],
defaultZoom: 7,
focusZoom: 14
};
var LEAFLET_MODUS = 'lokal'; // 'cdn' zum Testen, 'lokal' für Produktion
var LEAFLET_LOKAL_BASIS = '/resources/lib/leaflet/';
var LEAFLET_QUELLEN = {
cdn: {
css: [
'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css',
'https://unpkg.com/leaflet.fullscreen@5.3.1/dist/Control.FullScreen.css'
],
js: [
'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js',
'https://unpkg.com/leaflet.fullscreen@5.3.1/dist/Control.FullScreen.umd.js'
]
},
lokal: {
css: [
LEAFLET_LOKAL_BASIS + 'leaflet.css',
LEAFLET_LOKAL_BASIS + 'Control.FullScreen.css'
],
js: [
LEAFLET_LOKAL_BASIS + 'leaflet.js',
LEAFLET_LOKAL_BASIS + 'Control.FullScreen.umd.js'
]
}
};
/* ====================================================================
* 2) HINTERGRUNDKARTEN
*
* basemap.at: seit 2023 neue Service-URLs unter mapsneu.wien.gv.at.
* Quelle: https://cdn.basemap.at/basemap.at_URL_Umstellung_2023.pdf
* ==================================================================== */
function baueLayer() {
var osmStandard = L.tileLayer(
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
{ maxZoom: 19, attribution: '© OpenStreetMap-Mitwirkende' }
);
var osmRelief = L.tileLayer(
'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
{
maxZoom: 17,
attribution: 'Kartendaten: © OpenStreetMap-Mitwirkende, SRTM | ' +
'Darstellung: © OpenTopoMap (CC-BY-SA)'
}
);
var luftbild = L.tileLayer(
'https://mapsneu.wien.gv.at/basemap/bmaporthofoto30cm/' +
'normal/google3857/{z}/{y}/{x}.jpeg',
{
maxZoom: 19,
attribution: 'Datenquelle: <a href="https://basemap.at">basemap.at</a>'
}
);
var overlay = L.tileLayer(
'https://mapsneu.wien.gv.at/basemap/bmapoverlay/' +
'normal/google3857/{z}/{y}/{x}.png',
{
maxZoom: 19,
attribution: 'Datenquelle: <a href="https://basemap.at">basemap.at</a>'
}
);
var gemischt = L.layerGroup( [ luftbild, overlay ] );
return {
basis: {
'Luftbild + Karte': gemischt,
'Luftbild': luftbild,
'Karte': osmStandard,
'TopoKarte': tracestrack
},
standard: gemischt
};
}
/* ====================================================================
* 3) PIN-ICONS
* ==================================================================== */
function icon( aktiv ) {
return L.divIcon( {
className: 'wikimap-pin' + ( aktiv ? ' wikimap-pin-aktiv' : '' ),
iconSize: aktiv ? [ 28, 28 ] : [ 18, 18 ],
iconAnchor: aktiv ? [ 14, 28 ] : [ 9, 18 ],
popupAnchor: [ 0, aktiv ? -28 : -18 ]
} );
}
/* ====================================================================
* 4) DATEN AUS WIKI-JSON-SEITE
* ==================================================================== */
function ladeJson( seite ) {
var api = new mw.Api();
return api.get( {
action: 'query',
prop: 'revisions',
titles: seite,
rvprop: 'content',
rvslots: 'main',
formatversion: 2
} ).then( function ( data ) {
var page = data.query.pages[ 0 ];
if ( !page || page.missing || !page.revisions ) {
throw new Error( 'Seite fehlt: ' + seite );
}
return JSON.parse( page.revisions[ 0 ].slots.main.content );
} );
}
/* ====================================================================
* 5) EINE EINZELNE KARTE BAUEN
* ==================================================================== */
function baueKarte( container, orte, gemeinden ) {
var aktivId = container.getAttribute( 'data-aktiv-ort' ) || null;
var filter = container.getAttribute( 'data-filter' ) || null;
var hoehe = container.getAttribute( 'data-hoehe' );
if ( hoehe ) {
container.style.height = parseInt( hoehe, 10 ) + 'px';
}
var hatFullscreen = !!( L.Control && L.Control.FullScreen );
var layer = baueLayer();
var map = L.map( container, {
center: CONFIG.defaultCenter,
zoom: CONFIG.defaultZoom,
layers: [ layer.standard ],
fullscreenControl: hatFullscreen
} );
L.control.layers( layer.basis ).addTo( map );
// Sicherheitsnetz: Knopf explizit hinzufügen, falls die Option oben
// nicht gegriffen hat (z.B. Plugin kam minimal verspätet).
if ( hatFullscreen && !map.fullscreenControl ) {
map.addControl( new L.Control.FullScreen() );
}
if ( container.getAttribute( 'data-gemeinden' ) === 'ja' && gemeinden ) {
L.geoJSON( gemeinden, {
style: { color: '#3367d6', weight: 1.5, fillOpacity: 0.05 },
onEachFeature: function ( feature, lyr ) {
var p = feature.properties || {};
var name = p.name || 'Gemeinde';
var ziel = p.seite || name;
lyr.bindTooltip( name );
lyr.on( 'click', function () {
window.location.href = mw.util.getUrl( ziel );
} );
lyr.on( 'mouseover', function () {
lyr.setStyle( { fillOpacity: 0.25 } );
} );
lyr.on( 'mouseout', function () {
lyr.setStyle( { fillOpacity: 0.05 } );
} );
}
} ).addTo( map );
}
var aktiverMarker = null;
var sichtbare = [];
orte.forEach( function ( ort ) {
if ( filter && ort.kategorie !== filter ) {
return;
}
if ( typeof ort.lat !== 'number' || typeof ort.lon !== 'number' ) {
return;
}
var istAktiv = aktivId && ort.id === aktivId;
var m = L.marker( [ ort.lat, ort.lon ], {
icon: icon( istAktiv ),
zIndexOffset: istAktiv ? 1000 : 0
} );
var link = mw.util.getUrl( ort.seite || ort.name );
m.bindPopup(
'<b>' + mw.html.escape( ort.name || '' ) + '</b><br>' +
'<a href="' + link + '">Zur Seite →</a>'
);
m.addTo( map );
sichtbare.push( [ ort.lat, ort.lon ] );
if ( istAktiv ) {
aktiverMarker = m;
}
} );
if ( aktiverMarker ) {
map.setView( aktiverMarker.getLatLng(), CONFIG.focusZoom );
aktiverMarker.openPopup();
} else if ( sichtbare.length ) {
map.fitBounds( sichtbare, { padding: [ 30, 30 ] } );
}
map.on( 'resize fullscreenchange', function () {
setTimeout( function () {
map.invalidateSize();
}, 100 );
} );
return map;
}
/* ====================================================================
* 6) EINSTIEG
* ==================================================================== */
function init() {
var container = document.querySelectorAll( '.wikimap' );
if ( !container.length ) {
return;
}
var brauchtGemeinden = Array.prototype.some.call(
container,
function ( c ) {
return c.getAttribute( 'data-gemeinden' ) === 'ja';
}
);
var aufgaben = [ ladeJson( CONFIG.orteSeite ) ];
aufgaben.push(
brauchtGemeinden ?
ladeJson( CONFIG.gemeindenSeite ).catch( function () {
return null;
} ) :
$.Deferred().resolve( null )
);
$.when.apply( $, aufgaben ).then( function ( orteData, gemeinden ) {
var orte = ( orteData && orteData.orte ) || [];
Array.prototype.forEach.call( container, function ( c ) {
try {
baueKarte( c, orte, gemeinden );
} catch ( e ) {
c.textContent = 'Karte konnte nicht geladen werden.';
mw.log.error( e );
}
} );
} ).catch( function ( e ) {
Array.prototype.forEach.call( container, function ( c ) {
c.textContent = 'Ortsdaten konnten nicht geladen werden.';
} );
mw.log.error( e );
} );
}
/* ====================================================================
* 7) LEAFLET SICHERSTELLEN, DANN STARTEN
* ==================================================================== */
// Lädt EIN Skript nach. Wichtig: manche Leaflet-Plugins sind UMD-Module
// und registrieren sich bei vorhandenem AMD-Loader (z.B. ResourceLoader)
// NICHT am globalen L. Wir deaktivieren define.amd kurzzeitig, damit das
// Plugin den globalen-L-Pfad nimmt.
function ladeSkript( src ) {
return $.Deferred( function ( d ) {
var amd = window.define;
var hatAmd = amd && amd.amd;
if ( hatAmd ) {
window.define = undefined; // AMD kurz ausblenden
}
var s = document.createElement( 'script' );
s.src = src;
s.onload = function () {
if ( hatAmd ) {
window.define = amd; // AMD wiederherstellen
}
d.resolve();
};
s.onerror = function () {
if ( hatAmd ) {
window.define = amd;
}
mw.log.error( 'WikiMap: Datei nicht ladbar: ' + src );
d.resolve(); // trotzdem weitermachen
};
document.head.appendChild( s );
} ).promise();
}
function ladeCss( href ) {
var l = document.createElement( 'link' );
l.rel = 'stylesheet';
l.href = href;
document.head.appendChild( l );
}
function stelleLeafletBereit( fertig ) {
var q = LEAFLET_QUELLEN[ LEAFLET_MODUS ];
// CSS immer einhängen (auch wenn L schon da ist, könnte Plugin-CSS fehlen).
q.css.forEach( ladeCss );
// JS-Dateien strikt nacheinander laden und ERST starten, wenn sowohl
// L.map als auch L.Control.FullScreen existieren.
var kette = $.Deferred().resolve().promise();
q.js.forEach( function ( src ) {
kette = kette.then( function () {
// Leaflet-Core nur laden, wenn noch nicht da.
if ( /leaflet\.js$/.test( src ) && window.L && window.L.map ) {
return;
}
return ladeSkript( src );
} );
} );
kette.then( function () {
// Sicherheitsnetz: kurz warten, falls Plugin minimal verzögert ist.
var versuche = 0;
( function pruefe() {
if ( window.L && window.L.map ) {
fertig();
} else if ( versuche++ < 50 ) {
setTimeout( pruefe, 50 );
} else {
mw.log.error( 'WikiMap: Leaflet nicht verfügbar.' );
}
}() );
} );
}
mw.loader.using( [ 'mediawiki.api', 'mediawiki.util' ] ).then( function () {
$( function () {
if ( !document.querySelector( '.wikimap' ) ) {
return;
}
stelleLeafletBereit( init );
} );
} );
}() );