Keine Bearbeitungszusammenfassung
Keine Bearbeitungszusammenfassung
Markierung: Zurückgesetzt
Zeile 1: Zeile 1:
/**
/**
  * Gadget-WikiMap.js
  * Eventform-Gadget: Komfortables Anlegen von Veranstaltungsseiten
* ============================================================================
  * Aktiv auf der Seite "Veranstaltungen"
* 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. Mehrere Kategorien
*                    kommagetrennt moeglich, z.B. "asiatisch,gasthaus,imbiss"
*  data-pin        Ad-hoc-Pin: "lat,lon" zeigt GENAU EINEN Pin an diesen
*                    Koordinaten und KEINE Orte aus der zentralen JSON.
*                    Fuer Veranstaltungsseiten (Eventform) gedacht.
*  data-pin-name    Optional zum Pin: Text im Popup.
*  data-gemeinden  "ja" => Gemeinde-Umrisse einblenden (Klick => Seite)
*  data-nur-aktiv  "ja" => nur den aktiven Ort zeigen, alle anderen Pins
*                    ausblenden (braucht data-aktiv-ort)
*  data-layer      Start-Ansicht per Menü-Name, z.B. "Zug" oder
*                    "Zug + Luftbild". Unbekannter Name => normaler Default.
*  data-hoehe      Pixel-Höhe der Karte (optional, sonst CSS-Default)
*
* ORTE-FELDER (WikiMap-Orte.json)
* -------------------------------
*  id        Pflicht. Anzeigename im Popup UND Ziel-Wikiseite beim Klick.
*  geo        Koordinaten als String "lat,lon" – direkt aus OSM.org oder
*              OrganicMaps kopierbar, Leerzeichen nach dem Komma erlaubt.
*              Beispiel: "47.7073509,15.9933643"
*  lat, lon  Alternative zu geo: Koordinaten als zwei Zahlenfelder.
*  kategorie  Optional. Für Filter und Farben.
*  seite      Optional. Übersteuert das Klick-Ziel. Wiki-Seitenname ODER
*              externe Adresse (beginnend mit http:// oder https://).
*
* ABHÄNGIGKEIT: Leaflet 1.9.x + Leaflet.fullscreen. Dieses Gadget lädt sie
* bei Bedarf selbst (siehe LEAFLET_QUELLEN unten).
* ============================================================================
  */
  */
( function () {
( function () {
'use strict';
'use strict';


/* ====================================================================
// Konfiguration: auf welcher Seite soll der Button erscheinen?
* 1) KONFIGURATION
var TRIGGER_PAGE = 'Veranstaltungen';
* ==================================================================== */
var CONFIG = {
orteSeite: 'MediaWiki:WikiMap-Orte.json',
gemeindenSeite: 'MediaWiki:WikiMap-Gemeinden.json',
defaultCenter: [ 47.6, 16.1 ],
defaultZoom: 7,
focusZoom: 14
};


var LEAFLET_MODUS = 'lokal'; // 'cdn' zum Testen, 'lokal' für Produktion
if ( mw.config.get( 'wgPageName' ) !== TRIGGER_PAGE ) {
var LEAFLET_LOKAL_BASIS = '/resources/lib/leaflet/';
return;
}
 
if ( !mw.config.get( 'wgUserName' ) ) {
return;
}
 
function v( val ) {
return ( val || '' ).trim();
}


var LEAFLET_QUELLEN = {
/**
cdn: {
* Parst gängige Koordinaten-Formate.
css: [
* Gibt { lat, lng } zurück oder null bei ungültiger Eingabe.
'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css',
*/
'https://unpkg.com/leaflet.fullscreen@5.3.1/dist/Control.FullScreen.css'
function parseCoordinates( input ) {
],
if ( !input ) {
js: [
return null;
'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'
]
}
}
};
var s = input.trim();
var m;


/* ====================================================================
// Deutsches Format: Komma als Dezimaltrenner
* 2) HINTERGRUNDKARTEN
// "47,7073132, 15,9934565" / "47,7073132; 15,9934565" / "47,7073132 15,9934565"
*
m = /^(-?\d+,\d+)\s*[;\s]\s*(-?\d+,\d+)$/.exec( s ) ||
* basemap.at: seit 2023 neue Service-URLs unter mapsneu.wien.gv.at.
/^(-?\d+,\d+),\s+(-?\d+,\d+)$/.exec( s );
* Quelle: https://cdn.basemap.at/basemap.at_URL_Umstellung_2023.pdf
if ( m ) {
*
var latDE = parseFloat( m[ 1 ].replace( ',', '.' ) );
* Tracestrack: API-Schlüssel auf Referer schwarzatal.org beschränkt.
var lngDE = parseFloat( m[ 2 ].replace( ',', '.' ) );
* Quelle: https://console.tracestrack.com/
if ( isFinite( latDE ) && isFinite( lngDE ) &&
* ==================================================================== */
Math.abs( latDE ) <= 90 && Math.abs( lngDE ) <= 180 ) {
function baueLayer() {
return { lat: latDE, lng: lngDE };
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 tracestrack = L.tileLayer(
 
'https://tile.tracestrack.com/topo__/{z}/{x}/{y}.png' +
// Internationales Dezimal: "47.75,15.68" / "47.75 15.68" / "47.75; 15.68" / "-47.75,15.68"
'?key=bc0464a46d41b1107569bd81112f05ab',
m = /^(-?\d+(?:\.\d+)?)\s*[,;\s]\s*(-?\d+(?:\.\d+)?)$/.exec( s );
{
if ( m ) {
maxZoom: 19,
var lat = parseFloat( m[ 1 ] );
attribution: '© <a href="https://www.tracestrack.com/">Tracestrack</a>, ' +
var lng = parseFloat( m[ 2 ] );
'© OpenStreetMap-Mitwirkende'
if ( isFinite( lat ) && isFinite( lng ) &&
Math.abs( lat ) <= 90 && Math.abs( lng ) <= 180 ) {
return { lat: lat, lng: lng };
}
}
);
}
var luftbild = L.tileLayer(
 
'https://mapsneu.wien.gv.at/basemap/bmaporthofoto30cm/' +
// Dezimal mit NSEW: "47.75N 15.68E" / "47.75°N, 15.68°E" (auch mit Dezimal-Komma: "47,75N 15,68E")
'normal/google3857/{z}/{y}/{x}.jpeg',
m = /^(\d+(?:[.,]\d+)?)\s*°?\s*([NS])\s*[,;\s]?\s*(\d+(?:[.,]\d+)?)\s*°?\s*([EW])$/i.exec( s );
{
if ( m ) {
maxZoom: 19,
var lat2 = parseFloat( m[ 1 ].replace( ',', '.' ) );
attribution: 'Datenquelle: <a href="https://basemap.at">basemap.at</a>'
if ( m[ 2 ].toUpperCase() === 'S' ) { lat2 = -lat2; }
var lng2 = parseFloat( m[ 3 ].replace( ',', '.' ) );
if ( m[ 4 ].toUpperCase() === 'W' ) { lng2 = -lng2; }
if ( isFinite( lat2 ) && isFinite( lng2 ) &&
Math.abs( lat2 ) <= 90 && Math.abs( lng2 ) <= 180 ) {
return { lat: lat2, lng: lng2 };
}
}
);
}
var overlay = L.tileLayer(
 
'https://mapsneu.wien.gv.at/basemap/bmapoverlay/' +
// DMS: 47°45'00.0"N 15°40'48.0"E
'normal/google3857/{z}/{y}/{x}.png',
// Akzeptiert gerade (' ") und typografische (’ ” ″ ′) Zeichen
{
m = /^(\d+)\s*°\s*(\d+)\s*['\u2019\u2032]\s*(\d+(?:[.,]\d+)?)\s*["\u201D\u2033]\s*([NS])\s*[,;\s]?\s*(\d+)\s*°\s*(\d+)\s*['\u2019\u2032]\s*(\d+(?:[.,]\d+)?)\s*["\u201D\u2033]\s*([EW])$/i.exec( s );
maxZoom: 19,
if ( m ) {
attribution: 'Datenquelle: <a href="https://basemap.at">basemap.at</a>'
var lat3 = parseInt( m[ 1 ], 10 ) + parseInt( m[ 2 ], 10 ) / 60 + parseFloat( m[ 3 ].replace( ',', '.' ) ) / 3600;
if ( m[ 4 ].toUpperCase() === 'S' ) { lat3 = -lat3; }
var lng3 = parseInt( m[ 5 ], 10 ) + parseInt( m[ 6 ], 10 ) / 60 + parseFloat( m[ 7 ].replace( ',', '.' ) ) / 3600;
if ( m[ 8 ].toUpperCase() === 'W' ) { lng3 = -lng3; }
if ( Math.abs( lat3 ) <= 90 && Math.abs( lng3 ) <= 180 ) {
return { lat: lat3, lng: lng3 };
}
}
);
}
// Zweite Luftbild-Instanz für die Gruppe, damit Layer-Wechsel nicht
// zwischen Gruppe und Solo-Luftbild kollidieren (Race Condition).
var luftbildKopie = 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 gemischt = L.layerGroup( [ luftbildKopie, overlay ] );


// OpenRailwayMap ist ein transparentes Gleis-Overlay und braucht eine
// DM: 47°45.000'N 15°40.800'E (auch typografische Zeichen)
// Basiskarte darunter. Eigene OSM-Instanz für die Gruppe (gleiches
m = /^(\d+)\s*°\s*(\d+(?:[.,]\d+)?)\s*['\u2019\u2032]\s*([NS])\s*[,;\s]?\s*(\d+)\s*°\s*(\d+(?:[.,]\d+)?)\s*['\u2019\u2032]\s*([EW])$/i.exec( s );
// Race-Condition-Muster wie beim Luftbild). Der Tileserver liefert
if ( m ) {
// 512px-Kacheln, daher tileSize/zoomOffset. Die a/b/c-Subdomains sind
var lat4 = parseInt( m[ 1 ], 10 ) + parseFloat( m[ 2 ].replace( ',', '.' ) ) / 60;
// deprecated, daher URL ohne {s}.
if ( m[ 3 ].toUpperCase() === 'S' ) { lat4 = -lat4; }
var osmFuerBahn = L.tileLayer(
var lng4 = parseInt( m[ 4 ], 10 ) + parseFloat( m[ 5 ].replace( ',', '.' ) ) / 60;
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
if ( m[ 6 ].toUpperCase() === 'W' ) { lng4 = -lng4; }
{ maxZoom: 19, attribution: '© OpenStreetMap-Mitwirkende' }
if ( Math.abs( lat4 ) <= 90 && Math.abs( lng4 ) <= 180 ) {
);
return { lat: lat4, lng: lng4 };
var bahnOverlay = L.tileLayer(
'https://tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png',
{
maxZoom: 19,
tileSize: 512,
zoomOffset: -1,
attribution: 'Overlay: <a href="https://www.openrailwaymap.org/">OpenRailwayMap</a> (CC-BY-SA 2.0)'
}
}
);
var eisenbahn = L.layerGroup( [ osmFuerBahn, bahnOverlay ] );
// Hilfsfunktionen für frische Instanzen: jede LayerGroup braucht
// EIGENE Tile-Layer, sonst kollidieren die Gruppen beim Umschalten
// (Race Condition, siehe luftbildKopie oben).
function neuesLuftbild() {
return 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>'
}
);
}
function neuesBmapOverlay() {
return 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>'
}
);
}
function neuesBahnOverlay() {
return L.tileLayer(
'https://tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png',
{
maxZoom: 19,
tileSize: 512,
zoomOffset: -1,
attribution: 'Overlay: <a href="https://www.openrailwaymap.org/">OpenRailwayMap</a> (CC-BY-SA 2.0)'
}
);
}
function neuesOsm() {
return L.tileLayer(
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
{ maxZoom: 19, attribution: '© OpenStreetMap-Mitwirkende' }
);
}
function neuesOpenTopo() {
return L.tileLayer(
'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
{
maxZoom: 17,
attribution: 'Kartendaten: © OpenStreetMap-Mitwirkende, SRTM | ' +
'Darstellung: © OpenTopoMap (CC-BY-SA)'
}
);
}
function neuesTracestrack() {
return L.tileLayer(
'https://tile.tracestrack.com/topo__/{z}/{x}/{y}.png' +
'?key=bc0464a46d41b1107569bd81112f05ab',
{
maxZoom: 19,
attribution: '© <a href="https://www.tracestrack.com/">Tracestrack</a>, ' +
'© OpenStreetMap-Mitwirkende'
}
);
}
}


// Zug-Kombis: jeweils Basiskarte(n) unten, Gleise obendrauf.
return null;
var zugLuftbildKarte = L.layerGroup(
}
[ neuesLuftbild(), neuesBmapOverlay(), neuesBahnOverlay() ]
);
var zugLuftbild = L.layerGroup( [ neuesLuftbild(), neuesBahnOverlay() ] );
var zugKarte = eisenbahn; // OSM + Gleise (bereits oben gebaut)
var zugTopo = L.layerGroup( [ neuesOpenTopo(), neuesBahnOverlay() ] );
var zugTopo2 = L.layerGroup( [ neuesTracestrack(), neuesBahnOverlay() ] );


// Bus: ÖPNVKarte (memomaps.de) ist eine VOLLSTÄNDIGE Karte mit
function fmtCoord( n ) {
// Buslinien/Haltestellen, KEIN transparentes Overlay. Deshalb gibt
return n.toFixed( 7 );
// es keine Bus+Luftbild-Kombis. Hinweis: Datenpflege des Projekts
}
// ist laut OSM-Wiki unregelmäßig.
var bus = L.tileLayer(
'https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png',
{
maxZoom: 18,
attribution: 'Karte: <a href="https://memomaps.de/">memomaps.de</a> ' +
'(CC-BY-SA), Daten: © OpenStreetMap-Mitwirkende'
}
);


return {
function buildOsmUrl( c ) {
basis: {
var lat = fmtCoord( c.lat );
'Luftbild + Karte': gemischt,
var lng = fmtCoord( c.lng );
'Luftbild': luftbild,
return 'https://www.openstreetmap.org/?mlat=' + lat + '&mlon=' + lng + '#map=17/' + lat + '/' + lng;
'Karte': osmStandard,
'Topokarte': osmRelief,
'Topokarte2': tracestrack,
'Zug + Luftbild + Karte': zugLuftbildKarte,
'Zug + Luftbild': zugLuftbild,
'Zug + Karte': zugKarte,
'Zug + Topokarte': zugTopo,
'Zug + Topokarte2': zugTopo2,
'Bus': bus
},
standard: gemischt
};
}
}


/* ====================================================================
function buildGoogleMapsUrl( c ) {
* 3) PIN-ICONS
return 'https://www.google.com/maps?q=' + fmtCoord( c.lat ) + ',' + fmtCoord( c.lng );
* ==================================================================== */
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 ]
} );
}
}


/* ====================================================================
function buildCoMapsUrl( c, titel ) {
* 4) DATEN AUS WIKI-JSON-SEITE
// Offizielles CoMaps-Deep-Link-Format (siehe https://comaps.at/api):
* ==================================================================== */
// https-Link, der im Browser die Web-Ansicht öffnet und auf Android
function ladeJson( seite ) {
// direkt die installierte CoMaps-App startet, mit Pin und Titel.
var api = new mw.Api();
var url = 'https://comaps.at/map?v=1&ll=' +
return api.get( {
fmtCoord( c.lat ) + ',' + fmtCoord( c.lng );
action: 'query',
if ( titel ) {
prop: 'revisions',
url += '&n=' + encodeURIComponent( titel );
titles: seite,
}
rvprop: 'content',
return url;
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 );
} );
}
}


/* ====================================================================
function buildWikitext( f ) {
* 5) EINE EINZELNE KARTE BAUEN
var lines = [];
* ==================================================================== */
lines.push( '[[Hauptseite]] > [[Hauptseite#Kultur & Events|Kultur & Events]] > [[Veranstaltungen]]' );
function baueKarte( container, orte, gemeinden, kategorien ) {
var aktivId = container.getAttribute( 'data-aktiv-ort' ) || null;


// Ad-hoc-Pin-Modus: data-pin="lat,lon" zeigt GENAU EINEN Pin an
// Zeit
// diesen Koordinaten und KEINE Orte aus der zentralen JSON.
lines.push( '==Zeit==' );
// Gedacht z.B. für Veranstaltungsseiten (Eventform), deren Ort
lines.push( '🗓️ ' + f.datum );
// nicht in WikiMap-Orte.json gepflegt wird.
if ( f.uhrzeit ) {
// Optional: data-pin-name für den Popup-Text.
lines.push( '' );
var pinAttr = container.getAttribute( 'data-pin' ) || '';
lines.push( '⏰ ' + f.uhrzeit );
var pinKoord = null;
}
if ( pinAttr ) {
lines.push( '<br>' );
var pinTeile = pinAttr.split( ',' );
lines.push( '<br>' );
if ( pinTeile.length === 2 ) {
lines.push( '<br>' );
var pLat = parseFloat( pinTeile[ 0 ] );
lines.push( '<br>' );
var pLon = parseFloat( pinTeile[ 1 ] );
 
if ( !isNaN( pLat ) && !isNaN( pLon ) ) {
// Event
pinKoord = [ pLat, pLon ];
lines.push( '==Event==' );
}
if ( f.bild ) {
lines.push( '[[file:' + f.bild + '|frameless|upright=3.0]]' );
lines.push( '' );
}
if ( f.websiteUrl ) {
lines.push( '🌐 ' + f.websiteUrl );
}
lines.push( '<br>' );
lines.push( '<br>' );
lines.push( '<br>' );
lines.push( '<br>' );
 
// Location - jede Symbol-Zeile in eigenem Absatz (Leerzeile dazwischen)
lines.push( '==Location==' );
// Karte mit dem Veranstaltungsort als einzigem Pin (WikiMap-Gadget).
// Nutzt die geparsten Koordinaten, damit alle Eingabeformate
// (deutsches Format, DMS, ...) funktionieren.
if ( f.coords ) {
lines.push( '<div class="wikimap" data-pin="' +
fmtCoord( f.coords.lat ) + ',' + fmtCoord( f.coords.lng ) +
'" data-pin-name="' + ( f.Adresse || f.titel || 'Veranstaltungsort' ) +
'"></div>' );
lines.push( '' );
}
var locFirst = true;
function addLocLine( line ) {
if ( !locFirst ) {
lines.push( '' );
}
}
lines.push( line );
locFirst = false;
}
if ( f.Adresse ) {
addLocLine( '📍 ' + f.Adresse );
}
if ( f.osmUrl ) {
addLocLine( '[[file:osm.png|24px|link=]] ' + f.osmUrl );
}
if ( f.koordinaten ) {
addLocLine( '🧭 ' + f.koordinaten );
}
if ( f.comapsUrl ) {
addLocLine( '[[file:CoMaps.png|24px|link=]] ' + f.comapsUrl );
}
}
var pinName = container.getAttribute( 'data-pin-name' ) || '';
if ( f.gmapsUrl ) {
addLocLine( '[[file:GoogleMaps.png|24px|link=]] ' + f.gmapsUrl );
}
lines.push( '<br>' );
lines.push( '<br>' );
lines.push( '<br>' );
lines.push( '<br>' );


// Nur den aktiven Ort zeigen (alle anderen Pins ausblenden).
// Photos & Videos
var nurAktiv = container.getAttribute( 'data-nur-aktiv' ) === 'ja';
lines.push( '==Photos & Videos==' );
lines.push( 'Bitte hier verlinken.' );


// Start-Layer per Menü-Name wählen, z.B. data-layer="Eisenbahnkarte".
return lines.join( '\n' );
// Unbekannter Name => normaler Default.
}
var layerWunsch = container.getAttribute( 'data-layer' ) || '';


// Filter: eine oder mehrere Kategorien, kommagetrennt.
mw.loader.using( [
// "asiatisch,gasthaus" => nur Orte, deren kategorie EINE davon ist.
'oojs-ui-core',
// Leer/nicht gesetzt => alle Orte.
'oojs-ui-windows',
var filterAttr = container.getAttribute( 'data-filter' ) || '';
'oojs-ui.styles.icons-interactions',
var filterListe = filterAttr
'mediawiki.api',
.split( ',' )
'mediawiki.util'
.map( function ( s ) { return s.trim(); } )
] ).then( function () {
.filter( function ( s ) { return s.length > 0; } );


var hoehe = container.getAttribute( 'data-hoehe' );
function EventDialog( config ) {
if ( hoehe ) {
EventDialog.super.call( this, config );
container.style.height = parseInt( hoehe, 10 ) + 'px';
}
}
OO.inheritClass( EventDialog, OO.ui.ProcessDialog );


var hatFullscreen = !!( L.Control && L.Control.FullScreen );
EventDialog.static.name = 'eventDialog';
EventDialog.static.title = 'Neue Veranstaltung anlegen';
EventDialog.static.actions = [
{ action: 'save', label: 'Seite anlegen', flags: [ 'primary', 'progressive' ] },
{ label: 'Abbrechen', flags: 'safe' }
];


var layer = baueLayer();
EventDialog.prototype.initialize = function () {
// Start-Layer: per data-layer gewünschter Menü-Name, sonst Default.
EventDialog.super.prototype.initialize.apply( this, arguments );
var startLayer = ( layerWunsch && layer.basis[ layerWunsch ] ) ?
 
layer.basis[ layerWunsch ] : layer.standard;
this.panel = new OO.ui.PanelLayout( { padded: true, expanded: false } );
var map = L.map( container, {
 
center: CONFIG.defaultCenter,
this.titelInput = new OO.ui.TextInputWidget( {
zoom: CONFIG.defaultZoom,
placeholder: 'z.B. Sommerfest 2026', required: true
layers: [ startLayer ],
} );
fullscreenControl: hatFullscreen
this.datumInput = new OO.ui.TextInputWidget( { type: 'date', required: true } );
} );
this.uhrzeitInput = new OO.ui.TextInputWidget( { type: 'time', value: '17:00' } );
L.control.layers( layer.basis ).addTo( map );


// Sicherheitsnetz: Knopf explizit hinzufügen, falls die Option oben
this.bildInput = new OO.ui.TextInputWidget( { placeholder: 'sommerfest.jpg' } );
// nicht gegriffen hat (z.B. Plugin kam minimal verspätet).
this.websiteUrlInput = new OO.ui.TextInputWidget( { placeholder: 'https://...' } );
if ( hatFullscreen && !map.fullscreenControl ) {
map.addControl( new L.Control.FullScreen() );
}


if ( container.getAttribute( 'data-gemeinden' ) === 'ja' && gemeinden ) {
this.AdresseInput = new OO.ui.TextInputWidget( { placeholder: 'Nussbaum Straße ' } );
L.geoJSON( gemeinden, {
this.osmUrlInput = new OO.ui.TextInputWidget( { placeholder: 'https://www.openstreetmap.org/way/...' } );
style: { color: '#3367d6', weight: 1.5, fillOpacity: 0.05 },
this.koordinatenInput = new OO.ui.TextInputWidget( { placeholder: '47.7500000,15.6800000' } );
onEachFeature: function ( feature, lyr ) {
this.comapsUrlInput = new OO.ui.TextInputWidget( { placeholder: 'https://comaps.at/...' } );
var p = feature.properties || {};
this.gmapsUrlInput = new OO.ui.TextInputWidget( { placeholder: 'https://maps.app.goo.gl/...' } );
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 fsBasis = new OO.ui.FieldsetLayout( { label: 'Pflichtfelder' } );
var sichtbare = [];
fsBasis.addItems( [
new OO.ui.FieldLayout( this.titelInput, {
label: 'Seitentitel', align: 'left',
help: 'Wird der Name der neuen Wiki-Seite. Muss eindeutig sein.'
} ),
new OO.ui.FieldLayout( this.datumInput, { label: 'Datum', align: 'left' } ),
new OO.ui.FieldLayout( this.uhrzeitInput, {
label: 'Uhrzeit', align: 'left',
help: 'Optional. Leer lassen wenn keine Uhrzeit gewünscht.'
} )
] );


// Ad-hoc-Pin-Modus: nur dieser eine Pin, keine Orte aus der JSON.
var fsEvent = new OO.ui.FieldsetLayout( { label: 'Event' } );
if ( pinKoord ) {
fsEvent.addItems( [
var pinMarker = L.marker( pinKoord, {
new OO.ui.FieldLayout( this.bildInput, {
icon: icon( true ),
label: 'Bild (Dateiname)', align: 'left',
zIndexOffset: 1000
help: 'Wenn man die Dateiendung weiß (z.B. jpg), wähle einen sinnvollen Namen mit der richtigen Datei-Endung (z.B. Sommerfest2026.jpg ) wenn mann dann später auf der fertigen Seite darauf klickt, kann man auch das Bild hochladen. Leer lassen, wenn kein Bild.'
} );
} ),
if ( pinName ) {
new OO.ui.FieldLayout( this.websiteUrlInput, { label: 'Website URL', align: 'left' } )
pinMarker.bindPopup( '<b>' + mw.html.escape( pinName ) + '</b>' );
] );
}
pinMarker.addTo( map );
map.setView( pinKoord, CONFIG.focusZoom );
if ( pinName ) {
pinMarker.openPopup();
}


map.on( 'resize fullscreenchange', function () {
var fsLoc = new OO.ui.FieldsetLayout( { label: 'Location (alles optional)' } );
setTimeout( function () {
fsLoc.addItems( [
map.invalidateSize();
new OO.ui.FieldLayout( this.AdresseInput, { label: 'Adresse', align: 'left' } ),
}, 100 );
new OO.ui.FieldLayout( this.osmUrlInput, {
} );
label: 'OpenStreetMap URL', align: 'left',
return map;
help: 'Leer lassen, dann wird bei gegebenen Koordinaten automatisch ein Link generiert.'
}
} ),
new OO.ui.FieldLayout( this.koordinatenInput, {
label: 'GPS-Koordinaten', align: 'left',
help: 'Akzeptiert u.a.: 47.75,15.68 · 47,7073132, 15,9934565 (deutsches Format) · 47.75N 15.68E · 47°45\'00"N 15°40\'48"E. Wenn gesetzt, werden für leere OSM/CoMaps/Google Maps Felder automatisch Links generiert und eine Karte auf der Seite eingebunden.'
} ),
new OO.ui.FieldLayout( this.comapsUrlInput, {
label: 'CoMaps URL', align: 'left',
help: 'Leer lassen, dann wird bei gegebenen Koordinaten automatisch ein Link generiert.'
} ),
new OO.ui.FieldLayout( this.gmapsUrlInput, {
label: 'Google Maps URL', align: 'left',
help: 'Leer lassen, dann wird bei gegebenen Koordinaten automatisch ein Link generiert.'
} )
] );


orte.forEach( function ( ort ) {
this.panel.$element.append( fsBasis.$element, fsEvent.$element, fsLoc.$element );
// Nur-aktiv-Modus: alles außer dem aktiven Ort überspringen.
this.$body.append( this.panel.$element );
if ( nurAktiv && ( !aktivId || ort.id !== aktivId ) ) {
};
return;
}
// Filter: wenn eine Filterliste gesetzt ist, muss die Kategorie
// des Ortes in der Liste vorkommen. Sonst wird der Ort ausgelassen.
if ( filterListe.length && filterListe.indexOf( ort.kategorie ) === -1 ) {
return;
}


// Koordinaten: bevorzugt aus dem geo-Feld (String "lat,lon",
EventDialog.prototype.getActionProcess = function ( action ) {
// direkt aus OSM/OrganicMaps kopierbar, Leerzeichen egal),
var dialog = this;
// sonst aus den klassischen lat/lon-Zahlenfeldern.
if ( action === 'save' ) {
var lat = ort.lat;
return new OO.ui.Process( function () {
var lon = ort.lon;
var titel = v( dialog.titelInput.getValue() );
if ( typeof ort.geo === 'string' ) {
var datum = v( dialog.datumInput.getValue() );
var teile = ort.geo.split( ',' );
var uhrzeit = v( dialog.uhrzeitInput.getValue() );
if ( teile.length === 2 ) {
lat = parseFloat( teile[ 0 ] );
lon = parseFloat( teile[ 1 ] );
}
}
if ( typeof lat !== 'number' || typeof lon !== 'number' ||
isNaN( lat ) || isNaN( lon ) ) {
return;
}
var istAktiv = aktivId && ort.id === aktivId;
// id ist Anzeigename und Ziel. seite (optional) übersteuert das
// Ziel; beginnt sie mit http:// oder https://, wird direkt auf
// die externe Adresse verlinkt statt auf eine Wiki-Seite.
var ziel = ort.seite || ort.id || '';
var istExtern = /^https?:\/\//i.test( ziel );
var link = istExtern ? ziel : mw.util.getUrl( ziel );
var m = L.marker( [ lat, lon ], {
icon: icon( istAktiv ),
zIndexOffset: istAktiv ? 1000 : 0
} );


// Kategorie-Farbe (nur für nicht-aktive Pins; aktiver bleibt rot).
if ( !titel || !datum ) {
// Wird gesetzt, sobald das Marker-DOM-Element existiert.
return $.Deferred().reject( new OO.ui.Error(
var farbe = kategorien && ort.kategorie ? kategorien[ ort.kategorie ] : null;
'Bitte Seitentitel und Datum ausfüllen.',
if ( farbe && !istAktiv ) {
{ recoverable: true }
m.on( 'add', function () {
) ).promise();
if ( this._icon ) {
this._icon.style.background = farbe;
}
}
} );
}


m.bindPopup(
var fields = {
'<b>' + mw.html.escape( ort.id || '' ) + '</b><br>' +
datum: datum,
'<a href="' + link + '"' +
uhrzeit: uhrzeit,
( istExtern ? ' target="_blank" rel="noopener"' : '' ) +
bild: v( dialog.bildInput.getValue() ),
'>Zur Seite →</a>'
websiteUrl: v( dialog.websiteUrlInput.getValue() ),
);
Adresse: v( dialog.AdresseInput.getValue() ),
m.addTo( map );
osmUrl: v( dialog.osmUrlInput.getValue() ),
sichtbare.push( [ lat, lon ] );
koordinaten: v( dialog.koordinatenInput.getValue() ),
if ( istAktiv ) {
comapsUrl: v( dialog.comapsUrlInput.getValue() ),
aktiverMarker = m;
gmapsUrl: v( dialog.gmapsUrlInput.getValue() )
}
};
} );


if ( aktiverMarker ) {
// Auto-URL-Generierung aus Koordinaten für leere Felder
map.setView( aktiverMarker.getLatLng(), CONFIG.focusZoom );
var coords = parseCoordinates( fields.koordinaten );
aktiverMarker.openPopup();
fields.coords = coords;
} else if ( sichtbare.length ) {
fields.titel = titel;
map.fitBounds( sichtbare, { padding: [ 30, 30 ] } );
if ( coords ) {
}
if ( !fields.osmUrl ) {
fields.osmUrl = buildOsmUrl( coords );
}
if ( !fields.comapsUrl ) {
fields.comapsUrl = buildCoMapsUrl( coords, titel );
}
if ( !fields.gmapsUrl ) {
fields.gmapsUrl = buildGoogleMapsUrl( coords );
}
}


map.on( 'resize fullscreenchange', function () {
var wikitext = buildWikitext( fields );
setTimeout( function () {
var api = new mw.Api();
map.invalidateSize();
}, 100 );
} );
return map;
}


/* ====================================================================
return api.get( {
* 6) EINSTIEG
action: 'query',
* ==================================================================== */
titles: titel,
function init() {
formatversion: 2
var container = document.querySelectorAll( '.wikimap' );
} ).then( function ( data ) {
if ( !container.length ) {
var page = data.query.pages[ 0 ];
return;
if ( !page.missing ) {
}
return $.Deferred().reject( new OO.ui.Error(
var brauchtGemeinden = Array.prototype.some.call(
'Eine Seite mit dem Titel „' + titel + '" existiert bereits.',
container,
{ recoverable: true }
function ( c ) {
) ).promise();
return c.getAttribute( 'data-gemeinden' ) === 'ja';
}
return api.postWithToken( 'csrf', {
action: 'edit',
title: titel,
text: wikitext,
createonly: true,
summary: 'Veranstaltung über Eventform-Gadget angelegt'
} );
} ).then( function () {
window.location.href = mw.util.getUrl( titel );
}, function ( err ) {
if ( err instanceof OO.ui.Error ) {
return $.Deferred().reject( err ).promise();
}
return $.Deferred().reject( new OO.ui.Error(
'Fehler beim Anlegen: ' + ( err && err.toString ? err.toString() : 'unbekannt' ),
{ recoverable: true }
) ).promise();
} );
} );
}
}
);
return EventDialog.super.prototype.getActionProcess.call( this, action );
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 ) || [];
// Optionaler Farb-Block: Kategorie -> Farbe. Fehlt er, bleiben
// die Pins in der Standardfarbe.
var kategorien = ( orteData && orteData.kategorien ) || {};
Array.prototype.forEach.call( container, function ( c ) {
try {
baueKarte( c, orte, gemeinden, kategorien );
} 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 );
} );
}


/* ====================================================================
EventDialog.prototype.getBodyHeight = function () {
* 7) LEAFLET SICHERSTELLEN, DANN STARTEN
return 600;
* ==================================================================== */
};


// Lädt EIN Skript nach. Wichtig: manche Leaflet-Plugins sind UMD-Module
var $container = $( '#event-form-container' );
// und registrieren sich bei vorhandenem AMD-Loader (z.B. ResourceLoader)
if ( !$container.length ) {
// NICHT am globalen L. Wir deaktivieren define.amd kurzzeitig, damit das
$container = $( '<div id="event-form-container"></div>' )
// Plugin den globalen-L-Pfad nimmt.
.appendTo( '#mw-content-text .mw-parser-output' );
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 button = new OO.ui.ButtonWidget( {
var l = document.createElement( 'link' );
label: 'Neue Veranstaltung anlegen',
l.rel = 'stylesheet';
flags: [ 'primary', 'progressive' ],
l.href = href;
icon: 'add'
document.head.appendChild( l );
} );
}


function stelleLeafletBereit( fertig ) {
var windowManager = new OO.ui.WindowManager();
var q = LEAFLET_QUELLEN[ LEAFLET_MODUS ];
$( document.body ).append( windowManager.$element );
var dialog = new EventDialog( { size: 'large' } );
windowManager.addWindows( [ dialog ] );


// CSS immer einhängen (auch wenn L schon da ist, könnte Plugin-CSS fehlen).
button.on( 'click', function () {
q.css.forEach( ladeCss );
windowManager.openWindow( dialog );
 
// 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 () {
$container.empty().append( button.$element );
// 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 );
} );
} );
} );
}() );
}() );

Version vom 4. Juli 2026, 22:33 Uhr

/**
 * Eventform-Gadget: Komfortables Anlegen von Veranstaltungsseiten
 * Aktiv auf der Seite "Veranstaltungen"
 */
( function () {
	'use strict';

	// Konfiguration: auf welcher Seite soll der Button erscheinen?
	var TRIGGER_PAGE = 'Veranstaltungen';

	if ( mw.config.get( 'wgPageName' ) !== TRIGGER_PAGE ) {
		return;
	}

	if ( !mw.config.get( 'wgUserName' ) ) {
		return;
	}

	function v( val ) {
		return ( val || '' ).trim();
	}

	/**
	 * Parst gängige Koordinaten-Formate.
	 * Gibt { lat, lng } zurück oder null bei ungültiger Eingabe.
	 */
	function parseCoordinates( input ) {
		if ( !input ) {
			return null;
		}
		var s = input.trim();
		var m;

		// Deutsches Format: Komma als Dezimaltrenner
		// "47,7073132, 15,9934565" / "47,7073132; 15,9934565" / "47,7073132 15,9934565"
		m = /^(-?\d+,\d+)\s*[;\s]\s*(-?\d+,\d+)$/.exec( s ) ||
			/^(-?\d+,\d+),\s+(-?\d+,\d+)$/.exec( s );
		if ( m ) {
			var latDE = parseFloat( m[ 1 ].replace( ',', '.' ) );
			var lngDE = parseFloat( m[ 2 ].replace( ',', '.' ) );
			if ( isFinite( latDE ) && isFinite( lngDE ) &&
				Math.abs( latDE ) <= 90 && Math.abs( lngDE ) <= 180 ) {
				return { lat: latDE, lng: lngDE };
			}
		}

		// Internationales Dezimal: "47.75,15.68" / "47.75 15.68" / "47.75; 15.68" / "-47.75,15.68"
		m = /^(-?\d+(?:\.\d+)?)\s*[,;\s]\s*(-?\d+(?:\.\d+)?)$/.exec( s );
		if ( m ) {
			var lat = parseFloat( m[ 1 ] );
			var lng = parseFloat( m[ 2 ] );
			if ( isFinite( lat ) && isFinite( lng ) &&
				Math.abs( lat ) <= 90 && Math.abs( lng ) <= 180 ) {
				return { lat: lat, lng: lng };
			}
		}

		// Dezimal mit NSEW: "47.75N 15.68E" / "47.75°N, 15.68°E" (auch mit Dezimal-Komma: "47,75N 15,68E")
		m = /^(\d+(?:[.,]\d+)?)\s*°?\s*([NS])\s*[,;\s]?\s*(\d+(?:[.,]\d+)?)\s*°?\s*([EW])$/i.exec( s );
		if ( m ) {
			var lat2 = parseFloat( m[ 1 ].replace( ',', '.' ) );
			if ( m[ 2 ].toUpperCase() === 'S' ) { lat2 = -lat2; }
			var lng2 = parseFloat( m[ 3 ].replace( ',', '.' ) );
			if ( m[ 4 ].toUpperCase() === 'W' ) { lng2 = -lng2; }
			if ( isFinite( lat2 ) && isFinite( lng2 ) &&
				Math.abs( lat2 ) <= 90 && Math.abs( lng2 ) <= 180 ) {
				return { lat: lat2, lng: lng2 };
			}
		}

		// DMS: 47°45'00.0"N 15°40'48.0"E
		// Akzeptiert gerade (' ") und typografische (’ ” ″ ′) Zeichen
		m = /^(\d+)\s*°\s*(\d+)\s*['\u2019\u2032]\s*(\d+(?:[.,]\d+)?)\s*["\u201D\u2033]\s*([NS])\s*[,;\s]?\s*(\d+)\s*°\s*(\d+)\s*['\u2019\u2032]\s*(\d+(?:[.,]\d+)?)\s*["\u201D\u2033]\s*([EW])$/i.exec( s );
		if ( m ) {
			var lat3 = parseInt( m[ 1 ], 10 ) + parseInt( m[ 2 ], 10 ) / 60 + parseFloat( m[ 3 ].replace( ',', '.' ) ) / 3600;
			if ( m[ 4 ].toUpperCase() === 'S' ) { lat3 = -lat3; }
			var lng3 = parseInt( m[ 5 ], 10 ) + parseInt( m[ 6 ], 10 ) / 60 + parseFloat( m[ 7 ].replace( ',', '.' ) ) / 3600;
			if ( m[ 8 ].toUpperCase() === 'W' ) { lng3 = -lng3; }
			if ( Math.abs( lat3 ) <= 90 && Math.abs( lng3 ) <= 180 ) {
				return { lat: lat3, lng: lng3 };
			}
		}

		// DM: 47°45.000'N 15°40.800'E (auch typografische Zeichen)
		m = /^(\d+)\s*°\s*(\d+(?:[.,]\d+)?)\s*['\u2019\u2032]\s*([NS])\s*[,;\s]?\s*(\d+)\s*°\s*(\d+(?:[.,]\d+)?)\s*['\u2019\u2032]\s*([EW])$/i.exec( s );
		if ( m ) {
			var lat4 = parseInt( m[ 1 ], 10 ) + parseFloat( m[ 2 ].replace( ',', '.' ) ) / 60;
			if ( m[ 3 ].toUpperCase() === 'S' ) { lat4 = -lat4; }
			var lng4 = parseInt( m[ 4 ], 10 ) + parseFloat( m[ 5 ].replace( ',', '.' ) ) / 60;
			if ( m[ 6 ].toUpperCase() === 'W' ) { lng4 = -lng4; }
			if ( Math.abs( lat4 ) <= 90 && Math.abs( lng4 ) <= 180 ) {
				return { lat: lat4, lng: lng4 };
			}
		}

		return null;
	}

	function fmtCoord( n ) {
		return n.toFixed( 7 );
	}

	function buildOsmUrl( c ) {
		var lat = fmtCoord( c.lat );
		var lng = fmtCoord( c.lng );
		return 'https://www.openstreetmap.org/?mlat=' + lat + '&mlon=' + lng + '#map=17/' + lat + '/' + lng;
	}

	function buildGoogleMapsUrl( c ) {
		return 'https://www.google.com/maps?q=' + fmtCoord( c.lat ) + ',' + fmtCoord( c.lng );
	}

	function buildCoMapsUrl( c, titel ) {
		// Offizielles CoMaps-Deep-Link-Format (siehe https://comaps.at/api):
		// https-Link, der im Browser die Web-Ansicht öffnet und auf Android
		// direkt die installierte CoMaps-App startet, mit Pin und Titel.
		var url = 'https://comaps.at/map?v=1&ll=' +
			fmtCoord( c.lat ) + ',' + fmtCoord( c.lng );
		if ( titel ) {
			url += '&n=' + encodeURIComponent( titel );
		}
		return url;
	}

	function buildWikitext( f ) {
		var lines = [];
		lines.push( '[[Hauptseite]] > [[Hauptseite#Kultur & Events|Kultur & Events]] > [[Veranstaltungen]]' );

		// Zeit
		lines.push( '==Zeit==' );
		lines.push( '🗓️ ' + f.datum );
		if ( f.uhrzeit ) {
			lines.push( '' );
			lines.push( '⏰ ' + f.uhrzeit );
		}
		lines.push( '<br>' );
		lines.push( '<br>' );
		lines.push( '<br>' );
		lines.push( '<br>' );

		// Event
		lines.push( '==Event==' );
		if ( f.bild ) {
			lines.push( '[[file:' + f.bild + '|frameless|upright=3.0]]' );
			lines.push( '' );
		}
		if ( f.websiteUrl ) {
			lines.push( '🌐 ' + f.websiteUrl );
		}
		lines.push( '<br>' );
		lines.push( '<br>' );
		lines.push( '<br>' );
		lines.push( '<br>' );

		// Location - jede Symbol-Zeile in eigenem Absatz (Leerzeile dazwischen)
		lines.push( '==Location==' );
		// Karte mit dem Veranstaltungsort als einzigem Pin (WikiMap-Gadget).
		// Nutzt die geparsten Koordinaten, damit alle Eingabeformate
		// (deutsches Format, DMS, ...) funktionieren.
		if ( f.coords ) {
			lines.push( '<div class="wikimap" data-pin="' +
				fmtCoord( f.coords.lat ) + ',' + fmtCoord( f.coords.lng ) +
				'" data-pin-name="' + ( f.Adresse || f.titel || 'Veranstaltungsort' ) +
				'"></div>' );
			lines.push( '' );
		}
		var locFirst = true;
		function addLocLine( line ) {
			if ( !locFirst ) {
				lines.push( '' );
			}
			lines.push( line );
			locFirst = false;
		}
		if ( f.Adresse ) {
			addLocLine( '📍 ' + f.Adresse );
		}
		if ( f.osmUrl ) {
			addLocLine( '[[file:osm.png|24px|link=]] ' + f.osmUrl );
		}
		if ( f.koordinaten ) {
			addLocLine( '🧭 ' + f.koordinaten );
		}
		if ( f.comapsUrl ) {
			addLocLine( '[[file:CoMaps.png|24px|link=]] ' + f.comapsUrl );
		}
		if ( f.gmapsUrl ) {
			addLocLine( '[[file:GoogleMaps.png|24px|link=]] ' + f.gmapsUrl );
		}
		lines.push( '<br>' );
		lines.push( '<br>' );
		lines.push( '<br>' );
		lines.push( '<br>' );

		// Photos & Videos
		lines.push( '==Photos & Videos==' );
		lines.push( 'Bitte hier verlinken.' );

		return lines.join( '\n' );
	}

	mw.loader.using( [
		'oojs-ui-core',
		'oojs-ui-windows',
		'oojs-ui.styles.icons-interactions',
		'mediawiki.api',
		'mediawiki.util'
	] ).then( function () {

		function EventDialog( config ) {
			EventDialog.super.call( this, config );
		}
		OO.inheritClass( EventDialog, OO.ui.ProcessDialog );

		EventDialog.static.name = 'eventDialog';
		EventDialog.static.title = 'Neue Veranstaltung anlegen';
		EventDialog.static.actions = [
			{ action: 'save', label: 'Seite anlegen', flags: [ 'primary', 'progressive' ] },
			{ label: 'Abbrechen', flags: 'safe' }
		];

		EventDialog.prototype.initialize = function () {
			EventDialog.super.prototype.initialize.apply( this, arguments );

			this.panel = new OO.ui.PanelLayout( { padded: true, expanded: false } );

			this.titelInput = new OO.ui.TextInputWidget( {
				placeholder: 'z.B. Sommerfest 2026', required: true
			} );
			this.datumInput = new OO.ui.TextInputWidget( { type: 'date', required: true } );
			this.uhrzeitInput = new OO.ui.TextInputWidget( { type: 'time', value: '17:00' } );

			this.bildInput = new OO.ui.TextInputWidget( { placeholder: 'sommerfest.jpg' } );
			this.websiteUrlInput = new OO.ui.TextInputWidget( { placeholder: 'https://...' } );

			this.AdresseInput = new OO.ui.TextInputWidget( { placeholder: 'Nussbaum Straße ' } );
			this.osmUrlInput = new OO.ui.TextInputWidget( { placeholder: 'https://www.openstreetmap.org/way/...' } );
			this.koordinatenInput = new OO.ui.TextInputWidget( { placeholder: '47.7500000,15.6800000' } );
			this.comapsUrlInput = new OO.ui.TextInputWidget( { placeholder: 'https://comaps.at/...' } );
			this.gmapsUrlInput = new OO.ui.TextInputWidget( { placeholder: 'https://maps.app.goo.gl/...' } );

			var fsBasis = new OO.ui.FieldsetLayout( { label: 'Pflichtfelder' } );
			fsBasis.addItems( [
				new OO.ui.FieldLayout( this.titelInput, {
					label: 'Seitentitel', align: 'left',
					help: 'Wird der Name der neuen Wiki-Seite. Muss eindeutig sein.'
				} ),
				new OO.ui.FieldLayout( this.datumInput, { label: 'Datum', align: 'left' } ),
				new OO.ui.FieldLayout( this.uhrzeitInput, {
					label: 'Uhrzeit', align: 'left',
					help: 'Optional. Leer lassen wenn keine Uhrzeit gewünscht.'
				} )
			] );

			var fsEvent = new OO.ui.FieldsetLayout( { label: 'Event' } );
			fsEvent.addItems( [
				new OO.ui.FieldLayout( this.bildInput, {
					label: 'Bild (Dateiname)', align: 'left',
					help: 'Wenn man die Dateiendung weiß (z.B. jpg), wähle einen sinnvollen Namen mit der richtigen Datei-Endung (z.B. Sommerfest2026.jpg ) wenn mann dann später auf der fertigen Seite darauf klickt, kann man auch das Bild hochladen. Leer lassen, wenn kein Bild.'
				} ),
				new OO.ui.FieldLayout( this.websiteUrlInput, { label: 'Website URL', align: 'left' } )
			] );

			var fsLoc = new OO.ui.FieldsetLayout( { label: 'Location (alles optional)' } );
			fsLoc.addItems( [
				new OO.ui.FieldLayout( this.AdresseInput, { label: 'Adresse', align: 'left' } ),
				new OO.ui.FieldLayout( this.osmUrlInput, {
					label: 'OpenStreetMap URL', align: 'left',
					help: 'Leer lassen, dann wird bei gegebenen Koordinaten automatisch ein Link generiert.'
				} ),
				new OO.ui.FieldLayout( this.koordinatenInput, {
					label: 'GPS-Koordinaten', align: 'left',
					help: 'Akzeptiert u.a.: 47.75,15.68 · 47,7073132, 15,9934565 (deutsches Format) · 47.75N 15.68E · 47°45\'00"N 15°40\'48"E. Wenn gesetzt, werden für leere OSM/CoMaps/Google Maps Felder automatisch Links generiert und eine Karte auf der Seite eingebunden.'
				} ),
				new OO.ui.FieldLayout( this.comapsUrlInput, {
					label: 'CoMaps URL', align: 'left',
					help: 'Leer lassen, dann wird bei gegebenen Koordinaten automatisch ein Link generiert.'
				} ),
				new OO.ui.FieldLayout( this.gmapsUrlInput, {
					label: 'Google Maps URL', align: 'left',
					help: 'Leer lassen, dann wird bei gegebenen Koordinaten automatisch ein Link generiert.'
				} )
			] );

			this.panel.$element.append( fsBasis.$element, fsEvent.$element, fsLoc.$element );
			this.$body.append( this.panel.$element );
		};

		EventDialog.prototype.getActionProcess = function ( action ) {
			var dialog = this;
			if ( action === 'save' ) {
				return new OO.ui.Process( function () {
					var titel = v( dialog.titelInput.getValue() );
					var datum = v( dialog.datumInput.getValue() );
					var uhrzeit = v( dialog.uhrzeitInput.getValue() );

					if ( !titel || !datum ) {
						return $.Deferred().reject( new OO.ui.Error(
							'Bitte Seitentitel und Datum ausfüllen.',
							{ recoverable: true }
						) ).promise();
					}

					var fields = {
						datum: datum,
						uhrzeit: uhrzeit,
						bild: v( dialog.bildInput.getValue() ),
						websiteUrl: v( dialog.websiteUrlInput.getValue() ),
						Adresse: v( dialog.AdresseInput.getValue() ),
						osmUrl: v( dialog.osmUrlInput.getValue() ),
						koordinaten: v( dialog.koordinatenInput.getValue() ),
						comapsUrl: v( dialog.comapsUrlInput.getValue() ),
						gmapsUrl: v( dialog.gmapsUrlInput.getValue() )
					};

					// Auto-URL-Generierung aus Koordinaten für leere Felder
					var coords = parseCoordinates( fields.koordinaten );
					fields.coords = coords;
					fields.titel = titel;
					if ( coords ) {
						if ( !fields.osmUrl ) {
							fields.osmUrl = buildOsmUrl( coords );
						}
						if ( !fields.comapsUrl ) {
							fields.comapsUrl = buildCoMapsUrl( coords, titel );
						}
						if ( !fields.gmapsUrl ) {
							fields.gmapsUrl = buildGoogleMapsUrl( coords );
						}
					}

					var wikitext = buildWikitext( fields );
					var api = new mw.Api();

					return api.get( {
						action: 'query',
						titles: titel,
						formatversion: 2
					} ).then( function ( data ) {
						var page = data.query.pages[ 0 ];
						if ( !page.missing ) {
							return $.Deferred().reject( new OO.ui.Error(
								'Eine Seite mit dem Titel „' + titel + '" existiert bereits.',
								{ recoverable: true }
							) ).promise();
						}
						return api.postWithToken( 'csrf', {
							action: 'edit',
							title: titel,
							text: wikitext,
							createonly: true,
							summary: 'Veranstaltung über Eventform-Gadget angelegt'
						} );
					} ).then( function () {
						window.location.href = mw.util.getUrl( titel );
					}, function ( err ) {
						if ( err instanceof OO.ui.Error ) {
							return $.Deferred().reject( err ).promise();
						}
						return $.Deferred().reject( new OO.ui.Error(
							'Fehler beim Anlegen: ' + ( err && err.toString ? err.toString() : 'unbekannt' ),
							{ recoverable: true }
						) ).promise();
					} );
				} );
			}
			return EventDialog.super.prototype.getActionProcess.call( this, action );
		};

		EventDialog.prototype.getBodyHeight = function () {
			return 600;
		};

		var $container = $( '#event-form-container' );
		if ( !$container.length ) {
			$container = $( '<div id="event-form-container"></div>' )
				.appendTo( '#mw-content-text .mw-parser-output' );
		}

		var button = new OO.ui.ButtonWidget( {
			label: 'Neue Veranstaltung anlegen',
			flags: [ 'primary', 'progressive' ],
			icon: 'add'
		} );

		var windowManager = new OO.ui.WindowManager();
		$( document.body ).append( windowManager.$element );
		var dialog = new EventDialog( { size: 'large' } );
		windowManager.addWindows( [ dialog ] );

		button.on( 'click', function () {
			windowManager.openWindow( dialog );
		} );

		$container.empty().append( button.$element );

	} );
}() );