Die Seite wurde neu angelegt: „/** * Gadget-Bahnsteiganzeiger.js * ============================================================================ * Bettet ÖBB-Live-Abfahrts-/Ankunftstafeln ("Liveticker") in Wiki-Seiten ein * und lädt sie selbstständig neu. Intern heißt das Ding "Bahnsteiganzeiger", * für Fahrgäste steht auf der Seite "Monitore". * * WARUM EIN GADGET? * MediaWiki entfernt rohe <iframe> aus dem Seitentext. Dieses Gadget baut das * iframe daher selbst aus ein…“
 
Keine Bearbeitungszusammenfassung
 
(3 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 1: Zeile 1:
/**
/**
  * Gadget-Bahnsteiganzeiger.js
  * Gadget-Bahnhof_anlegen.js
  * ============================================================================
  * ============================================================================
  * Bettet ÖBB-Live-Abfahrts-/Ankunftstafeln ("Liveticker") in Wiki-Seiten ein
  * Komfortables Anlegen von Bahnhofsseiten. Button erscheint nur auf der Seite
  * und lädt sie selbstständig neu. Intern heißt das Ding "Bahnsteiganzeiger",
  * "Bahnhof anlegen" und nur für angemeldete Nutzer.
* für Fahrgäste steht auf der Seite "Monitore".
  *
  *
  * WARUM EIN GADGET?
  * Erzeugt eine Seite "Bahnhof <Name>" mit:
  * MediaWiki entfernt rohe <iframe> aus dem Seitentext. Dieses Gadget baut das
  *   - Breadcrumb + 🌐 Website + Wikipedia
  * iframe daher selbst aus einem einfachen Container – und kann es zusätzlich
*  - WikiMap-Karte (aktiver Ort = Seitentitel, Filter "Bahnhof",
  * alle 60 s neu laden (was ein rohes iframe nie könnte).
*    Start-Layer "Zug + Luftbild + Karte")
*  - Standort-Zeilen (📍 Adresse, OSM, 🧭 Koordinaten, CoMaps, Google Maps)
  *     mit Auto-Links aus Koordinaten
  *   - Abschnitt "Monitore" mit zwei Bahnsteiganzeigern (dep/arr) und je einem
*    Link zur detaillierten ÖBB-Tafel
  *
  *
  * EINBINDUNG AUF EINER SEITE
  * Die evaId wird von Hand eingetragen ("Prüfen" öffnet die Monitore-Ansicht
*  <div class="bahnsteiganzeiger" data-evaid="1231805" data-typ="dep"></div>
  * dieser evaId zur Kontrolle). Die WikiMap-Orte.json wird NICHT angefasst
*  <div class="bahnsteiganzeiger" data-evaid="1231805" data-typ="arr"></div>
  * (pflegst du selbst).
*
* data-Attribute:
  *   data-evaid      Pflicht. Stationsnummer (evaId) des Bahnhofs.
*  data-typ        "dep" = Abfahrten (Standard), "arr" = Ankünfte.
*  data-anzahl    Zeilenzahl (Standard 40).
  *   data-hoehe      Pixel-Höhe der Tafel (Standard 600; bei mehr Zeilen als
*                  hineinpassen scrollt die Tafel innen).
*  data-intervall  Neulade-Intervall in Sekunden (Standard 60; 0 = aus).
  * ============================================================================
  * ============================================================================
  */
  */
Zeile 27: Zeile 22:
'use strict';
'use strict';


var BASIS = 'https://fahrplan.oebb.at/bin/stboard.exe/dn';
// Button nur auf dieser Seite (wgPageName nutzt Unterstriche).
var LIVETICKER = 'vs_scotty.vs_liveticker';
if ( mw.config.get( 'wgPageName' ) !== 'Bahnhof_anlegen' ) {
var PRODUCTS = '1111111111111'; // alle Verkehrsmittel inkl. Bus
return;
var STD_ANZAHL = 40;
}
var STD_HOEHE = 600;
if ( !mw.config.get( 'wgUserName' ) ) {
var STD_INTERVALL = 60; // Sekunden
return;
}


function baueUrl( eva, typ, anzahl ) {
// Start-Layer der Karte auf Bahnhofsseiten.
return BASIS +
var KARTE_LAYER = 'Zug + Luftbild + Karte';
'?L=' + LIVETICKER +
'&evaId=' + encodeURIComponent( eva ) +
'&boardType=' + typ +
'&productsFilter=' + PRODUCTS +
'&dirInput=' +
'&tickerID=' + typ +
'&start=yes' +
'&eqstops=true' +
'&showJourneys=' + anzahl +
'&additionalTime=0';
}


function titelText( typ ) {
function v( val ) {
return typ === 'arr' ? 'Ankünfte' : 'Abfahrten';
return ( val || '' ).trim();
}
}


function baueAnzeiger( container ) {
/* ------------------------------------------------------------------ *
var eva = ( container.getAttribute( 'data-evaid' ) || '' )
* Koordinaten-Parsing + Link-Bau.
.trim().replace( /^0+/, '' );
* ------------------------------------------------------------------ */
var typ = ( container.getAttribute( 'data-typ' ) || 'dep' ).toLowerCase();
function parseCoordinates( input ) {
if ( typ !== 'arr' ) {
if ( !input ) {
typ = 'dep';
return null;
}
}
var s = input.trim();
var m;


if ( !eva ) {
m = /^(-?\d+,\d+)\s*[;\s]\s*(-?\d+,\d+)$/.exec( s ) ||
container.textContent = 'Bahnsteiganzeiger: keine evaId angegeben.';
/^(-?\d+,\d+),\s+(-?\d+,\d+)$/.exec( s );
container.className += ' bahnsteiganzeiger-fehler';
if ( m ) {
return;
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 };
}
}
}


var anzahl = parseInt( container.getAttribute( 'data-anzahl' ), 10 ) || STD_ANZAHL;
m = /^(-?\d+(?:\.\d+)?)\s*[,;\s]\s*(-?\d+(?:\.\d+)?)$/.exec( s );
var hoehe = parseInt( container.getAttribute( 'data-hoehe' ), 10 ) || STD_HOEHE;
if ( m ) {
var intervall = container.getAttribute( 'data-intervall' );
var lat = parseFloat( m[ 1 ] );
intervall = ( intervall === null ) ? STD_INTERVALL : parseInt( intervall, 10 );
var lng = parseFloat( m[ 2 ] );
if ( isFinite( lat ) && isFinite( lng ) &&
Math.abs( lat ) <= 90 && Math.abs( lng ) <= 180 ) {
return { lat: lat, lng: lng };
}
}


container.textContent = '';
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 };
}
}


var titel = document.createElement( 'div' );
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 );
titel.className = 'bahnsteiganzeiger-titel';
if ( m ) {
titel.textContent = titelText( typ );
var lat3 = parseInt( m[ 1 ], 10 ) + parseInt( m[ 2 ], 10 ) / 60 + parseFloat( m[ 3 ].replace( ',', '.' ) ) / 3600;
container.appendChild( titel );
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 };
}
}


var rahmen = document.createElement( 'div' );
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 );
rahmen.className = 'bahnsteiganzeiger-rahmen';
if ( m ) {
rahmen.style.height = hoehe + 'px';
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 };
}
}


var iframe = document.createElement( 'iframe' );
return null;
iframe.className = 'bahnsteiganzeiger-iframe';
}
iframe.setAttribute( 'frameborder', '0' );
iframe.setAttribute( 'scrolling', 'auto' );
iframe.setAttribute( 'loading', 'lazy' );
iframe.title = titelText( typ );


var url = baueUrl( eva, typ, anzahl );
function fmtCoord( n ) {
function laden() {
return n.toFixed( 7 );
// Cache-Buster erzwingt frische Daten bei jedem Neuladen.
}
iframe.src = url + '&_ts=' + Date.now();
function buildOsmUrl( c ) {
var lat = fmtCoord( c.lat ), 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, name ) {
// CoMaps/Organic Maps: ll = Koordinaten, n = URL-kodierter Name.
return 'https://comaps.at/map?v=1&ll=' + fmtCoord( c.lat ) + ',' + fmtCoord( c.lng ) +
'&n=' + encodeURIComponent( name || '' );
}
 
/* ------------------------------------------------------------------ *
* ÖBB-Detaillink (volle Tafel). typ: "dep"/"arr" -> sqView 2/3.
* "mon" -> Monitore-Ansicht (sqView 1), für den Prüf-Knopf.
* ------------------------------------------------------------------ */
function baueDetailUrl( eva, typ ) {
var boardType = ( typ === 'arr' ) ? 'arr' : 'dep';
var sqView = ( typ === 'arr' ) ? '3' : ( typ === 'mon' ? '1' : '2' );
return 'https://fahrplan.oebb.at/bin/stboard.exe/dn?protocol=https:&input=' +
encodeURIComponent( eva ) +
'&boardType=' + boardType +
'&sqView=' + sqView +
'&productsFilter=1111111111111&maxJourneys=40&start=yes&newrequest=yes&';
}
 
/* ------------------------------------------------------------------ *
* Wikitext der Bahnhofsseite.
* Abstand: nach jedem BLOCK zwei <br> + Leerzeile. Die einzelnen
* Standort-Zeilen bleiben eng (nur je eine Leerzeile = eigener Absatz).
* ------------------------------------------------------------------ */
function buildWikitext( f ) {
var L = [];
 
function block() {
L.push( '<br><br>' );
L.push( '' );
}
}
laden();


rahmen.appendChild( iframe );
L.push( '[[Hauptseite]] > [[Hauptseite#Öffentlicher Verkehr|Öffentlicher Verkehr]] > [[Zug]] > [[Zug#Bahnhöfe|Bahnhöfe]]' );
container.appendChild( rahmen );
block();
 
if ( f.website ) {
L.push( '🌐 ' + f.website );
block();
}


if ( intervall > 0 ) {
if ( f.wikipedia ) {
var hinweis = document.createElement( 'div' );
L.push( '[[file:Wikipedia.png|24px|link=|Wikipedia]] ' + f.wikipedia );
hinweis.className = 'bahnsteiganzeiger-hinweis';
block();
hinweis.textContent = 'Aktualisiert automatisch (alle ' + intervall + ' s).';
container.appendChild( hinweis );
setInterval( laden, intervall * 1000 );
}
}
// Karte: aktiver Ort = Seitentitel, nur Bahnhof-Pins, Start-Layer.
L.push( '<div class="wikimap" data-aktiv-ort="' + f.titel +
'" data-filter="Bahnhof" data-layer="' + KARTE_LAYER + '"></div>' );
block();
// Standort-Zeilen – eng, jede in eigenem Absatz (Leerzeile dazwischen).
var hatStandort = false;
var first = true;
function loc( line ) {
if ( !first ) { L.push( '' ); }
L.push( line );
first = false;
hatStandort = true;
}
if ( f.adresse ) { loc( '📍 ' + f.adresse ); }
if ( f.osmUrl ) { loc( '[[file:osm.png|24px|link=]] ' + f.osmUrl ); }
if ( f.koordinaten ) { loc( '🧭 ' + f.koordinaten ); }
if ( f.comapsUrl ) { loc( '[[file:CoMaps.png|24px|link=]] ' + f.comapsUrl ); }
if ( f.gmapsUrl ) { loc( '[[file:GoogleMaps.png|24px|link=]] ' + f.gmapsUrl ); }
if ( hatStandort ) {
L.push( '<br><br>' );
block();
}
// Monitore (intern: Bahnsteiganzeiger). Link jeweils direkt unter dem Anzeiger.
L.push( '== Monitore ==' );
block();
L.push( '<div class="bahnsteiganzeiger" data-evaid="' + f.evaId + '" data-typ="dep"></div>' );
L.push( '' );
L.push( '[' + baueDetailUrl( f.evaId, 'dep' ) + ' Zur detaillierten Abfahrtstafel]' );
L.push( '<br><br>' );
block();
L.push( '<div class="bahnsteiganzeiger" data-evaid="' + f.evaId + '" data-typ="arr"></div>' );
L.push( '' );
L.push( '[' + baueDetailUrl( f.evaId, 'arr' ) + ' Zur detaillierten Ankunftstafel]' );
block();
return L.join( '\n' );
}
}


function init() {
/* ------------------------------------------------------------------ *
var liste = document.querySelectorAll( '.bahnsteiganzeiger' );
* Oberfläche.
Array.prototype.forEach.call( liste, function ( c ) {
* ------------------------------------------------------------------ */
try {
mw.loader.using( [
baueAnzeiger( c );
'oojs-ui-core',
} catch ( e ) {
'oojs-ui-windows',
c.textContent = 'Bahnsteiganzeiger konnte nicht geladen werden.';
'oojs-ui.styles.icons-interactions',
if ( window.mw && mw.log ) {
'oojs-ui.styles.icons-content',
mw.log.error( e );
'mediawiki.api',
}
'mediawiki.util'
] ).then( function () {
 
function BahnhofDialog( config ) {
BahnhofDialog.super.call( this, config );
}
OO.inheritClass( BahnhofDialog, OO.ui.ProcessDialog );
 
BahnhofDialog.static.name = 'bahnhofDialog';
BahnhofDialog.static.title = 'Neuen Bahnhof anlegen';
BahnhofDialog.static.actions = [
{ action: 'save', label: 'Seite anlegen', flags: [ 'primary', 'progressive' ] },
{ label: 'Abbrechen', flags: 'safe' }
];
 
BahnhofDialog.prototype.initialize = function () {
BahnhofDialog.super.prototype.initialize.apply( this, arguments );
var dialog = this;
 
this.panel = new OO.ui.PanelLayout( { padded: true, expanded: false } );
 
this.nameInput = new OO.ui.TextInputWidget( { placeholder: 'z.B. Pottschach', required: true } );
 
this.evaidInput = new OO.ui.TextInputWidget( { placeholder: 'z.B. 1131839', required: true } );
this.pruefButton = new OO.ui.ButtonWidget( { label: 'Prüfen', icon: 'linkExternal' } );
 
this.websiteInput = new OO.ui.TextInputWidget( { placeholder: 'https://bahnhof.oebb.at/…' } );
this.wikipediaInput = new OO.ui.TextInputWidget( { placeholder: 'https://de.wikipedia.org/wiki/…' } );
this.adresseInput = new OO.ui.TextInputWidget( { placeholder: '2630 Pottschach, Putzmannsdorfer Straße 2' } );
this.koordinatenInput = new OO.ui.TextInputWidget( { placeholder: '47.69694, 16.00583' } );
this.osmUrlInput = new OO.ui.TextInputWidget( { placeholder: 'https://www.openstreetmap.org/…' } );
this.comapsUrlInput = new OO.ui.TextInputWidget( { placeholder: 'https://comaps.at/…' } );
this.gmapsUrlInput = new OO.ui.TextInputWidget( { placeholder: 'https://maps.app.goo.gl/…' } );
 
// evaId im Browser prüfen -> Monitore-Ansicht (sqView=1).
this.pruefButton.on( 'click', function () {
var eva = v( dialog.evaidInput.getValue() ).replace( /^0+/, '' );
if ( !eva ) { return; }
window.open( baueDetailUrl( eva, 'mon' ), '_blank', 'noopener' );
} );
 
var fsPflicht = new OO.ui.FieldsetLayout( { label: 'Bahnhof (Pflicht)' } );
fsPflicht.addItems( [
new OO.ui.FieldLayout( this.nameInput, {
label: 'Bahnhofsname', align: 'left',
help: 'Wird zu „Bahnhof <Name>" – das ist Seitentitel UND Karten-ID (data-aktiv-ort). Der gleichnamige Eintrag muss in WikiMap-Orte.json existieren.'
} ),
new OO.ui.ActionFieldLayout( this.evaidInput, this.pruefButton, {
label: 'evaId', align: 'left',
help: 'Stationsnummer, nur Ziffern (z.B. 1131839). Findest du in der alten „Zug"-Vorlage oder in der ÖBB-Adresse hinter evaId=. „Prüfen" öffnet die Monitore-Ansicht dieser evaId in einem neuen Tab.'
} )
] );
 
var fsStandort = new OO.ui.FieldsetLayout( { label: 'Standort (optional)' } );
fsStandort.addItems( [
new OO.ui.FieldLayout( this.websiteInput, {
label: '🌐 Website', align: 'left',
help: 'z.B. die ÖBB-Bahnhofsseite. Steht als erste Zeile oben; das Panorama kommt später von Hand darüber.'
} ),
new OO.ui.FieldLayout( this.wikipediaInput, {
label: 'Wikipedia', align: 'left',
help: 'Link zum Wikipedia-Artikel des Bahnhofs (optional).'
} ),
new OO.ui.FieldLayout( this.adresseInput, { label: '📍 Adresse', align: 'left' } ),
new OO.ui.FieldLayout( this.koordinatenInput, {
label: '🧭 Koordinaten', align: 'left',
help: 'Akzeptiert u.a.: 47.69694, 16.00583 · deutsches Komma-Format · 47°45\'00"N 15°40\'48"E. Wenn gesetzt, werden für leere OSM/CoMaps/Google-Maps-Felder automatisch Links erzeugt. (Der Karten-Pin kommt aus WikiMap-Orte.json, nicht von hier.)'
} ),
new OO.ui.FieldLayout( this.osmUrlInput, {
label: 'OpenStreetMap-URL', align: 'left',
help: 'Leer lassen -> wird bei gegebenen Koordinaten automatisch erzeugt.'
} ),
new OO.ui.FieldLayout( this.comapsUrlInput, {
label: 'CoMaps-URL', align: 'left',
help: 'Leer lassen -> wird bei gegebenen Koordinaten automatisch erzeugt (mit dem Seitentitel als Pin-Namen).'
} ),
new OO.ui.FieldLayout( this.gmapsUrlInput, {
label: 'Google-Maps-URL', align: 'left',
help: 'Leer lassen -> wird bei gegebenen Koordinaten automatisch erzeugt.'
} )
] );
 
this.panel.$element.append( fsPflicht.$element, fsStandort.$element );
this.$body.append( this.panel.$element );
};
 
BahnhofDialog.prototype.getActionProcess = function ( action ) {
var dialog = this;
function fehler( msg ) {
return $.Deferred().reject( new OO.ui.Error( msg, { recoverable: true } ) ).promise();
}
if ( action === 'save' ) {
return new OO.ui.Process( function () {
var name = v( dialog.nameInput.getValue() );
var evaId = v( dialog.evaidInput.getValue() ).replace( /^0+/, '' );
 
if ( !name ) {
return fehler( 'Bitte den Bahnhofsnamen ausfüllen.' );
}
if ( !/^\d+$/.test( evaId ) ) {
return fehler( 'Bitte eine gültige evaId (nur Ziffern) eintragen.' );
}
 
var titel = /^Bahnhof\s/i.test( name ) ? name : 'Bahnhof ' + name;
 
var fields = {
titel: titel,
evaId: evaId,
website: v( dialog.websiteInput.getValue() ),
wikipedia: v( dialog.wikipediaInput.getValue() ),
adresse: v( dialog.adresseInput.getValue() ),
koordinaten: v( dialog.koordinatenInput.getValue() ),
osmUrl: v( dialog.osmUrlInput.getValue() ),
comapsUrl: v( dialog.comapsUrlInput.getValue() ),
gmapsUrl: v( dialog.gmapsUrlInput.getValue() )
};
 
var coords = parseCoordinates( fields.koordinaten );
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 fehler( 'Eine Seite „' + titel + '" existiert bereits.' );
}
return api.postWithToken( 'csrf', {
action: 'edit',
title: titel,
text: wikitext,
createonly: true,
summary: 'Bahnhofsseite über Bahnhof-anlegen-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 fehler( 'Fehler beim Anlegen: ' + ( err && err.toString ? err.toString() : 'unbekannt' ) );
} );
} );
}
}
return BahnhofDialog.super.prototype.getActionProcess.call( this, action );
};
BahnhofDialog.prototype.getBodyHeight = function () {
return 600;
};
var $container = $( '#bahnhof-form-container' );
if ( !$container.length ) {
$container = $( '<div id="bahnhof-form-container"></div>' )
.appendTo( '#mw-content-text .mw-parser-output' );
}
var button = new OO.ui.ButtonWidget( {
label: 'Neuen Bahnhof anlegen',
flags: [ 'primary', 'progressive' ],
icon: 'add'
} );
} );
}


$( function () {
var windowManager = new OO.ui.WindowManager();
if ( document.querySelector( '.bahnsteiganzeiger' ) ) {
$( document.body ).append( windowManager.$element );
init();
var dialog = new BahnhofDialog( { size: 'large' } );
}
windowManager.addWindows( [ dialog ] );
 
button.on( 'click', function () {
windowManager.openWindow( dialog );
} );
 
$container.empty().append( button.$element );
 
} );
} );
}() );
}() );

Aktuelle Version vom 9. Juli 2026, 21:08 Uhr

/**
 * Gadget-Bahnhof_anlegen.js
 * ============================================================================
 * Komfortables Anlegen von Bahnhofsseiten. Button erscheint nur auf der Seite
 * "Bahnhof anlegen" und nur für angemeldete Nutzer.
 *
 * Erzeugt eine Seite "Bahnhof <Name>" mit:
 *   - Breadcrumb + 🌐 Website + Wikipedia
 *   - WikiMap-Karte (aktiver Ort = Seitentitel, Filter "Bahnhof",
 *     Start-Layer "Zug + Luftbild + Karte")
 *   - Standort-Zeilen (📍 Adresse, OSM, 🧭 Koordinaten, CoMaps, Google Maps)
 *     mit Auto-Links aus Koordinaten
 *   - Abschnitt "Monitore" mit zwei Bahnsteiganzeigern (dep/arr) und je einem
 *     Link zur detaillierten ÖBB-Tafel
 *
 * Die evaId wird von Hand eingetragen ("Prüfen" öffnet die Monitore-Ansicht
 * dieser evaId zur Kontrolle). Die WikiMap-Orte.json wird NICHT angefasst
 * (pflegst du selbst).
 * ============================================================================
 */
( function () {
	'use strict';

	// Button nur auf dieser Seite (wgPageName nutzt Unterstriche).
	if ( mw.config.get( 'wgPageName' ) !== 'Bahnhof_anlegen' ) {
		return;
	}
	if ( !mw.config.get( 'wgUserName' ) ) {
		return;
	}

	// Start-Layer der Karte auf Bahnhofsseiten.
	var KARTE_LAYER = 'Zug + Luftbild + Karte';

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

	/* ------------------------------------------------------------------ *
	 * Koordinaten-Parsing + Link-Bau.
	 * ------------------------------------------------------------------ */
	function parseCoordinates( input ) {
		if ( !input ) {
			return null;
		}
		var s = input.trim();
		var m;

		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 };
			}
		}

		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 };
			}
		}

		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 };
			}
		}

		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 };
			}
		}

		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 ), 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, name ) {
		// CoMaps/Organic Maps: ll = Koordinaten, n = URL-kodierter Name.
		return 'https://comaps.at/map?v=1&ll=' + fmtCoord( c.lat ) + ',' + fmtCoord( c.lng ) +
			'&n=' + encodeURIComponent( name || '' );
	}

	/* ------------------------------------------------------------------ *
	 * ÖBB-Detaillink (volle Tafel). typ: "dep"/"arr" -> sqView 2/3.
	 * "mon" -> Monitore-Ansicht (sqView 1), für den Prüf-Knopf.
	 * ------------------------------------------------------------------ */
	function baueDetailUrl( eva, typ ) {
		var boardType = ( typ === 'arr' ) ? 'arr' : 'dep';
		var sqView = ( typ === 'arr' ) ? '3' : ( typ === 'mon' ? '1' : '2' );
		return 'https://fahrplan.oebb.at/bin/stboard.exe/dn?protocol=https:&input=' +
			encodeURIComponent( eva ) +
			'&boardType=' + boardType +
			'&sqView=' + sqView +
			'&productsFilter=1111111111111&maxJourneys=40&start=yes&newrequest=yes&';
	}

	/* ------------------------------------------------------------------ *
	 * Wikitext der Bahnhofsseite.
	 * Abstand: nach jedem BLOCK zwei <br> + Leerzeile. Die einzelnen
	 * Standort-Zeilen bleiben eng (nur je eine Leerzeile = eigener Absatz).
	 * ------------------------------------------------------------------ */
	function buildWikitext( f ) {
		var L = [];

		function block() {
			L.push( '<br><br>' );
			L.push( '' );
		}

		L.push( '[[Hauptseite]] > [[Hauptseite#Öffentlicher Verkehr|Öffentlicher Verkehr]] > [[Zug]] > [[Zug#Bahnhöfe|Bahnhöfe]]' );
		block();

		if ( f.website ) {
			L.push( '🌐 ' + f.website );
			block();
		}

		if ( f.wikipedia ) {
			L.push( '[[file:Wikipedia.png|24px|link=|Wikipedia]] ' + f.wikipedia );
			block();
		}

		// Karte: aktiver Ort = Seitentitel, nur Bahnhof-Pins, Start-Layer.
		L.push( '<div class="wikimap" data-aktiv-ort="' + f.titel +
			'" data-filter="Bahnhof" data-layer="' + KARTE_LAYER + '"></div>' );
		block();

		// Standort-Zeilen – eng, jede in eigenem Absatz (Leerzeile dazwischen).
		var hatStandort = false;
		var first = true;
		function loc( line ) {
			if ( !first ) { L.push( '' ); }
			L.push( line );
			first = false;
			hatStandort = true;
		}
		if ( f.adresse ) { loc( '📍 ' + f.adresse ); }
		if ( f.osmUrl ) { loc( '[[file:osm.png|24px|link=]] ' + f.osmUrl ); }
		if ( f.koordinaten ) { loc( '🧭 ' + f.koordinaten ); }
		if ( f.comapsUrl ) { loc( '[[file:CoMaps.png|24px|link=]] ' + f.comapsUrl ); }
		if ( f.gmapsUrl ) { loc( '[[file:GoogleMaps.png|24px|link=]] ' + f.gmapsUrl ); }
		if ( hatStandort ) {
			L.push( '<br><br>' );
			block();
		}

		// Monitore (intern: Bahnsteiganzeiger). Link jeweils direkt unter dem Anzeiger.
		L.push( '== Monitore ==' );
		block();
		L.push( '<div class="bahnsteiganzeiger" data-evaid="' + f.evaId + '" data-typ="dep"></div>' );
		L.push( '' );
		L.push( '[' + baueDetailUrl( f.evaId, 'dep' ) + ' Zur detaillierten Abfahrtstafel]' );
		L.push( '<br><br>' );
		block();
		L.push( '<div class="bahnsteiganzeiger" data-evaid="' + f.evaId + '" data-typ="arr"></div>' );
		L.push( '' );
		L.push( '[' + baueDetailUrl( f.evaId, 'arr' ) + ' Zur detaillierten Ankunftstafel]' );
		block();

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

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

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

		BahnhofDialog.static.name = 'bahnhofDialog';
		BahnhofDialog.static.title = 'Neuen Bahnhof anlegen';
		BahnhofDialog.static.actions = [
			{ action: 'save', label: 'Seite anlegen', flags: [ 'primary', 'progressive' ] },
			{ label: 'Abbrechen', flags: 'safe' }
		];

		BahnhofDialog.prototype.initialize = function () {
			BahnhofDialog.super.prototype.initialize.apply( this, arguments );
			var dialog = this;

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

			this.nameInput = new OO.ui.TextInputWidget( { placeholder: 'z.B. Pottschach', required: true } );

			this.evaidInput = new OO.ui.TextInputWidget( { placeholder: 'z.B. 1131839', required: true } );
			this.pruefButton = new OO.ui.ButtonWidget( { label: 'Prüfen', icon: 'linkExternal' } );

			this.websiteInput = new OO.ui.TextInputWidget( { placeholder: 'https://bahnhof.oebb.at/…' } );
			this.wikipediaInput = new OO.ui.TextInputWidget( { placeholder: 'https://de.wikipedia.org/wiki/…' } );
			this.adresseInput = new OO.ui.TextInputWidget( { placeholder: '2630 Pottschach, Putzmannsdorfer Straße 2' } );
			this.koordinatenInput = new OO.ui.TextInputWidget( { placeholder: '47.69694, 16.00583' } );
			this.osmUrlInput = new OO.ui.TextInputWidget( { placeholder: 'https://www.openstreetmap.org/…' } );
			this.comapsUrlInput = new OO.ui.TextInputWidget( { placeholder: 'https://comaps.at/…' } );
			this.gmapsUrlInput = new OO.ui.TextInputWidget( { placeholder: 'https://maps.app.goo.gl/…' } );

			// evaId im Browser prüfen -> Monitore-Ansicht (sqView=1).
			this.pruefButton.on( 'click', function () {
				var eva = v( dialog.evaidInput.getValue() ).replace( /^0+/, '' );
				if ( !eva ) { return; }
				window.open( baueDetailUrl( eva, 'mon' ), '_blank', 'noopener' );
			} );

			var fsPflicht = new OO.ui.FieldsetLayout( { label: 'Bahnhof (Pflicht)' } );
			fsPflicht.addItems( [
				new OO.ui.FieldLayout( this.nameInput, {
					label: 'Bahnhofsname', align: 'left',
					help: 'Wird zu „Bahnhof <Name>" – das ist Seitentitel UND Karten-ID (data-aktiv-ort). Der gleichnamige Eintrag muss in WikiMap-Orte.json existieren.'
				} ),
				new OO.ui.ActionFieldLayout( this.evaidInput, this.pruefButton, {
					label: 'evaId', align: 'left',
					help: 'Stationsnummer, nur Ziffern (z.B. 1131839). Findest du in der alten „Zug"-Vorlage oder in der ÖBB-Adresse hinter evaId=. „Prüfen" öffnet die Monitore-Ansicht dieser evaId in einem neuen Tab.'
				} )
			] );

			var fsStandort = new OO.ui.FieldsetLayout( { label: 'Standort (optional)' } );
			fsStandort.addItems( [
				new OO.ui.FieldLayout( this.websiteInput, {
					label: '🌐 Website', align: 'left',
					help: 'z.B. die ÖBB-Bahnhofsseite. Steht als erste Zeile oben; das Panorama kommt später von Hand darüber.'
				} ),
				new OO.ui.FieldLayout( this.wikipediaInput, {
					label: 'Wikipedia', align: 'left',
					help: 'Link zum Wikipedia-Artikel des Bahnhofs (optional).'
				} ),
				new OO.ui.FieldLayout( this.adresseInput, { label: '📍 Adresse', align: 'left' } ),
				new OO.ui.FieldLayout( this.koordinatenInput, {
					label: '🧭 Koordinaten', align: 'left',
					help: 'Akzeptiert u.a.: 47.69694, 16.00583 · deutsches Komma-Format · 47°45\'00"N 15°40\'48"E. Wenn gesetzt, werden für leere OSM/CoMaps/Google-Maps-Felder automatisch Links erzeugt. (Der Karten-Pin kommt aus WikiMap-Orte.json, nicht von hier.)'
				} ),
				new OO.ui.FieldLayout( this.osmUrlInput, {
					label: 'OpenStreetMap-URL', align: 'left',
					help: 'Leer lassen -> wird bei gegebenen Koordinaten automatisch erzeugt.'
				} ),
				new OO.ui.FieldLayout( this.comapsUrlInput, {
					label: 'CoMaps-URL', align: 'left',
					help: 'Leer lassen -> wird bei gegebenen Koordinaten automatisch erzeugt (mit dem Seitentitel als Pin-Namen).'
				} ),
				new OO.ui.FieldLayout( this.gmapsUrlInput, {
					label: 'Google-Maps-URL', align: 'left',
					help: 'Leer lassen -> wird bei gegebenen Koordinaten automatisch erzeugt.'
				} )
			] );

			this.panel.$element.append( fsPflicht.$element, fsStandort.$element );
			this.$body.append( this.panel.$element );
		};

		BahnhofDialog.prototype.getActionProcess = function ( action ) {
			var dialog = this;
			function fehler( msg ) {
				return $.Deferred().reject( new OO.ui.Error( msg, { recoverable: true } ) ).promise();
			}
			if ( action === 'save' ) {
				return new OO.ui.Process( function () {
					var name = v( dialog.nameInput.getValue() );
					var evaId = v( dialog.evaidInput.getValue() ).replace( /^0+/, '' );

					if ( !name ) {
						return fehler( 'Bitte den Bahnhofsnamen ausfüllen.' );
					}
					if ( !/^\d+$/.test( evaId ) ) {
						return fehler( 'Bitte eine gültige evaId (nur Ziffern) eintragen.' );
					}

					var titel = /^Bahnhof\s/i.test( name ) ? name : 'Bahnhof ' + name;

					var fields = {
						titel: titel,
						evaId: evaId,
						website: v( dialog.websiteInput.getValue() ),
						wikipedia: v( dialog.wikipediaInput.getValue() ),
						adresse: v( dialog.adresseInput.getValue() ),
						koordinaten: v( dialog.koordinatenInput.getValue() ),
						osmUrl: v( dialog.osmUrlInput.getValue() ),
						comapsUrl: v( dialog.comapsUrlInput.getValue() ),
						gmapsUrl: v( dialog.gmapsUrlInput.getValue() )
					};

					var coords = parseCoordinates( fields.koordinaten );
					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 fehler( 'Eine Seite „' + titel + '" existiert bereits.' );
						}
						return api.postWithToken( 'csrf', {
							action: 'edit',
							title: titel,
							text: wikitext,
							createonly: true,
							summary: 'Bahnhofsseite über Bahnhof-anlegen-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 fehler( 'Fehler beim Anlegen: ' + ( err && err.toString ? err.toString() : 'unbekannt' ) );
					} );
				} );
			}
			return BahnhofDialog.super.prototype.getActionProcess.call( this, action );
		};

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

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

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

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

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

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

	} );
}() );