Keine Bearbeitungszusammenfassung
Keine Bearbeitungszusammenfassung
 
Zeile 1: Zeile 1:
/**
/**
  * Gadget-Mastodon.js
  * Gadget-Bahnsteiganzeiger.js
  * ============================================================================
  * ============================================================================
  * Bettet Mastodon-Beiträge (samt Video oder anderen Medien) in Wiki-Seiten
  * Bettet ÖBB-Live-Abfahrts-/Ankunftstafeln ("Liveticker") in Wiki-Seiten ein
  * ein – instanzunabhängig, also mastodon.social genauso wie jede andere
  * und lädt sie selbstständig neu. Intern heißt das Ding "Bahnsteiganzeiger",
  * Instanz. Verwendet die eingebaute /embed-Ansicht von Mastodon.
  * für Fahrgäste steht auf der Seite "Monitore".
  *
  *
  * KEIN ZUSATZSKRIPT DES ANBIETERS
  * WARUM EIN GADGET?
  * Die offizielle Einbindung lädt embed.js der Instanz nach, nur um die Höhe
  * MediaWiki entfernt rohe <iframe> aus dem Seitentext. Dieses Gadget baut das
  * des Rahmens anzupassen. Das macht dieses Gadget selbst: Mastodon beantwortet
  * iframe daher selbst aus einem einfachen Container – und kann es zusätzlich
  * eine postMessage mit seiner tatsächlichen Höhe. Es wird also genau eine
  * alle 60 s neu laden (was ein rohes iframe nie könnte).
* Adresse der Instanz abgerufen und sonst nichts.
  *
  *
  * KEINE ZWEI-KLICK-SPERRE – warum nicht?
  * ZWEI-KLICK-SPERRE
  * Die /embed-Ansicht ist eine öffentliche, cachebare Seite ohne Werbung,
  * Die Tafel liegt auf fahrplan.oebb.at. Ein automatisch geladenes iframe
  * ohne Reichweitenmessung und ohne Profilbildung; für anonyme Besucher
* würde die IP-Adresse jedes Besuchers an die ÖBB übertragen, ohne dass er
  * setzt sie keine Cookies. Damit greift die Einwilligungspflicht des
  * das wollte. Deshalb erscheint zuerst eine Vorschaubox; erst der Klick baut
* §165 TKG nicht, und für die IP-Übertragung beim Abruf trägt das
  * die Verbindung auf. Auf Wunsch merkt sich der Browser die Freigabe für die
  * berechtigte Interesse wie beim CCC-Gadget. Anders bei fahrplan.oebb.at
  * laufende Sitzung (sessionStorage kein Cookie, kein Banner); sie gilt dann
* und makertube.net, die ihre Vorschaubox behalten haben.
  * für alle ÖBB-Tafeln des Wikis und endet mit dem Schließen des Browsers.
  *
  * Unter der geladenen Tafel steht ein Link zum Zurücknehmen.
* NOTAUSGANG: data-sperre="ja" schaltet für eine einzelne Einbettung doch
* eine Vorschaubox mit Ladeknopf davor – gedacht für Instanzen, deren
  * Betreiber du nicht einschätzen kannst.
  *
  *
  * EINBINDUNG AUF EINER SEITE
  * EINBINDUNG AUF EINER SEITE
  *  <div class="mastodon-embed"
  *  <div class="bahnsteiganzeiger" data-evaid="1231805" data-typ="dep"></div>
*        data-post="https://mastodon.social/@benutzer/123456789012345678"></div>
  *  <div class="bahnsteiganzeiger" data-evaid="1231805" data-typ="arr"></div>
*
* Alternativ getrennt:
  *  <div class="mastodon-embed" data-instanz="mastodon.social"
*        data-benutzer="benutzer" data-id="123456789012345678"></div>
  *
  *
  * data-Attribute:
  * data-Attribute:
  *  data-post     Vollständige Adresse des Beitrags (empfohlen).
  *  data-evaid     Pflicht. Stationsnummer (evaId) des Bahnhofs.
  *  data-instanz  Hostname der Instanz, falls data-post nicht genutzt wird.
  *  data-typ       "dep" = Abfahrten (Standard), "arr" = Ankünfte.
*  data-benutzer  Benutzername ohne @, zusammen mit data-instanz/data-id.
  *  data-anzahl     Zeilenzahl (Standard 40).
*  data-id       Beitrags-ID, zusammen mit data-instanz/data-benutzer.
  *  data-hoehe      Pixel-Höhe der Tafel (Standard 808; bei mehr Zeilen als
  *  data-width     Breite (Standard 100%, höchstens 42em).
  *                   hineinpassen scrollt die Tafel innen).
  *  data-height    Starthöhe in Pixeln (Standard 600). Die endgültige Höhe
  *  data-intervall Neulade-Intervall in Sekunden (Standard 60; 0 = aus).
  *                 meldet Mastodon selbst, sobald der Beitrag geladen ist.
  *  data-titel    Titelzeile des Rahmens, für Screenreader
  *                  (Standard: "Mastodon-Beitrag").
*  data-sperre    "ja" => Vorschaubox mit Ladeknopf davorschalten.
  * ============================================================================
  * ============================================================================
  */
  */
Zeile 48: Zeile 36:
'use strict';
'use strict';


var STD_HOEHE = 600;
var BASIS = 'https://fahrplan.oebb.at/bin/stboard.exe/dn';
var LIVETICKER = 'vs_scotty.vs_liveticker';
var PRODUCTS = '1111111111111'; // alle Verkehrsmittel inkl. Bus
var STD_ANZAHL = 40;
var STD_HOEHE = 808;
var STD_INTERVALL = 60; // Sekunden
 
/* --- Zwei-Klick-Sperre ------------------------------------------- */
var SPEICHER = 'wiki-freigabe-oebb';
var DATENSCHUTZ = 'https://www.oebb.at/de/datenschutz';


/* Laufende Rahmen für die Höhenmeldung: Kennnummer -> iframe */
/* Noch gesperrte Tafeln dieser Seite – damit ein "merken"-Klick auch
var rahmenListe = {};
* die zweite Tafel (Ankünfte) gleich mitfreigibt. */
var naechsteNummer = 0;
var wartende = [];
var hoertZu = false;


function stil( el, regeln ) {
function freigegeben() {
var name;
try {
for ( name in regeln ) {
return window.sessionStorage.getItem( SPEICHER ) === 'ja';
el.style[ name ] = regeln[ name ];
} catch ( e ) {
return false; // privater Modus o.Ä.: dann eben jedes Mal fragen
}
}
return el;
}
}


function masszahl( wert, standard ) {
function setzeFreigabe( ja ) {
if ( wert === undefined || wert === null || wert === '' ) {
try {
return standard;
if ( ja ) {
}
window.sessionStorage.setItem( SPEICHER, 'ja' );
var text = String( wert ).trim();
} else {
return /^\d+$/.test( text ) ? text + 'px' : text;
window.sessionStorage.removeItem( SPEICHER );
}
} catch ( e ) {}
}
}


/* ------------------------------------------------------------------ *
function gibAlleFrei() {
* Adresse des Beitrags auswerten.
var liste = wartende.slice();
* Erlaubt sind die üblichen Mastodon-Formen:
wartende.length = 0;
*  https://instanz/@benutzer/123…
liste.forEach( function ( fn ) {
*  https://instanz/@benutzer@andereinstanz/123…
fn();
*  https://instanz/users/benutzer/statuses/123…
} );
* Zurück kommt { host, seite, embed } oder null.
* ------------------------------------------------------------------ */
function leseAdresse( roh ) {
var text = String( roh || '' ).trim();
if ( !/^https:\/\//i.test( text ) ) {
return null;
}
var a = document.createElement( 'a' );
a.href = text;
if ( !a.hostname || a.protocol !== 'https:' ) {
return null;
}
var pfad = a.pathname.replace( /\/+$/, '' ).replace( /\/embed$/, '' );
if ( !/^\/(@[^/]+\/\d+|users\/[^/]+\/statuses\/\d+)$/.test( pfad ) ) {
return null;
}
var seite = 'https://' + a.hostname + pfad;
return { host: a.hostname, seite: seite, embed: seite + '/embed' };
}
}


/* ------------------------------------------------------------------ *
function stil( el, regeln ) {
* Höhenmeldung von Mastodon entgegennehmen. Das eingebettete Mastodon
var name;
* antwortet auf { type: "setHeight", id: … } mit derselben Nachricht
for ( name in regeln ) {
* samt height – so sparen wir uns das embed.js des Anbieters.
el.style[ name ] = regeln[ name ];
* ------------------------------------------------------------------ */
function starteZuhoerer() {
if ( hoertZu ) {
return;
}
}
hoertZu = true;
return el;
window.addEventListener( 'message', function ( e ) {
var daten = e.data || {};
if ( daten.type !== 'setHeight' || !rahmenListe[ daten.id ] ) {
return;
}
var hoehe = parseInt( daten.height, 10 );
if ( isFinite( hoehe ) && hoehe > 0 ) {
rahmenListe[ daten.id ].style.height = hoehe + 'px';
}
} );
}
}


/* --- Beitrag einhängen ------------------------------------------------ */
/* --- ÖBB-Adressen ------------------------------------------------- */
function lade( container, cfg ) {
function baueUrl( eva, typ, anzahl ) {
container.textContent = '';
return BASIS +
starteZuhoerer();
'?L=' + LIVETICKER +
 
'&evaId=' + encodeURIComponent( eva ) +
var nummer = 'wiki-mastodon-' + ( naechsteNummer++ );
'&boardType=' + typ +
 
'&productsFilter=' + PRODUCTS +
var iframe = document.createElement( 'iframe' );
'&dirInput=' +
iframe.src = cfg.embed;
'&tickerID=' + typ +
iframe.title = cfg.titel + ' (' + cfg.host + ')';
'&start=yes' +
iframe.setAttribute( 'frameborder', '0' );
'&eqstops=true' +
iframe.setAttribute( 'scrolling', 'no' );
'&showJourneys=' + anzahl +
iframe.setAttribute( 'allowfullscreen', 'allowfullscreen' );
'&additionalTime=0';
iframe.setAttribute( 'allow', 'fullscreen' );
}
iframe.setAttribute( 'sandbox',
'allow-same-origin allow-scripts allow-popups' );
iframe.setAttribute( 'loading', 'lazy' );
iframe.setAttribute( 'referrerpolicy', 'no-referrer' );
stil( iframe, {
width: cfg.breite,
maxWidth: '42em',
height: cfg.hoehe,
border: '0'
} );


rahmenListe[ nummer ] = iframe;
function titelText( typ ) {
iframe.addEventListener( 'load', function () {
return typ === 'arr' ? 'Ankünfte' : 'Abfahrten';
try {
iframe.contentWindow.postMessage(
{ type: 'setHeight', id: nummer }, 'https://' + cfg.host
);
} catch ( e ) {}
} );
 
container.appendChild( iframe );
 
// Rückfallebene: wer das iframe blockiert, bekommt wenigstens den Weg
// zum Beitrag.
var fuss = stil( document.createElement( 'div' ), {
fontSize: '85%',
color: '#54595d',
marginTop: '0.4em'
} );
var link = document.createElement( 'a' );
link.href = cfg.seite;
link.target = '_blank';
link.rel = 'noopener';
link.textContent = 'Beitrag auf ' + cfg.host + ' öffnen';
fuss.appendChild( link );
container.appendChild( fuss );
}
}


/* --- Notausgang: Vorschaubox statt sofortigem Laden -------------------- */
/* --- Vorschaubox (noch nichts geladen) ---------------------------- */
function sperre( container, cfg ) {
function sperre( container, cfg ) {
var erledigt = false;
container.textContent = '';
container.textContent = '';
container.className = container.className
.replace( / ?bahnsteiganzeiger-fehler/, '' );


var box = stil( document.createElement( 'div' ), {
var box = stil( document.createElement( 'div' ), {
Zeile 183: Zeile 120:
boxSizing: 'border-box'
boxSizing: 'border-box'
} );
} );
box.className = 'mastodon-embed-sperre';
box.className = 'bahnsteiganzeiger-sperre';


var kopf = stil( document.createElement( 'div' ), {
var kopf = stil( document.createElement( 'div' ), {
Zeile 190: Zeile 127:
marginBottom: '0.4em'
marginBottom: '0.4em'
} );
} );
kopf.textContent = cfg.titel + ' von ' + cfg.host;
kopf.textContent = titelText( cfg.typ ) + ' – Live-Tafel der ÖBB';
box.appendChild( kopf );
box.appendChild( kopf );


var text = stil( document.createElement( 'p' ), { margin: '0.4em 0' } );
var text = stil( document.createElement( 'p' ), { margin: '0.4em 0' } );
text.textContent = 'Der Beitrag wird direkt von der Mastodon-Instanz ' +
text.textContent = 'Die Tafel liegt auf fahrplan.oebb.at und gehört der ' +
cfg.host + ' geladen – einem Server, der nicht uns gehört. Dabei ' +
'ÖBB-Personenverkehr AG. Beim Laden erfährt die ÖBB deine ' +
'erfährt dessen Betreiber deine IP-Adresse und welchen Beitrag du ' +
'IP-Adresse, welchen Bahnhof du gerade ansiehst und technische ' +
'ansiehst. Vor deinem Klick geht keine Anfrage dorthin.';
'Angaben zu deinem Gerät und Browser. Solange du nicht klickst, ' +
'verlässt keine einzige Anfrage dieses Wiki.';
box.appendChild( text );
box.appendChild( text );


Zeile 206: Zeile 144:
} );
} );
knopf.type = 'button';
knopf.type = 'button';
knopf.textContent = 'Beitrag laden';
knopf.textContent = titelText( cfg.typ ) + ' laden';
knopf.addEventListener( 'click', function () {
 
lade( container, cfg );
var haken = document.createElement( 'input' );
haken.type = 'checkbox';
 
var hakenLabel = stil( document.createElement( 'label' ), {
display: 'block',
margin: '0.7em 0',
fontSize: '92%'
} );
} );
hakenLabel.appendChild( haken );
hakenLabel.appendChild( document.createTextNode(
' Für diese Sitzung merken – gilt dann für alle ÖBB-Tafeln im ' +
'Wiki und endet, sobald du den Browser schließt. Es wird kein ' +
'Cookie gesetzt.'
) );
box.appendChild( hakenLabel );
box.appendChild( stil( document.createElement( 'div' ),
box.appendChild( stil( document.createElement( 'div' ),
{ margin: '0.7em 0' } ) ).appendChild( knopf );
{ margin: '0.7em 0' } ) ).appendChild( knopf );
Zeile 218: Zeile 170:
marginTop: '0.6em'
marginTop: '0.6em'
} );
} );
var link = document.createElement( 'a' );
var dsLink = document.createElement( 'a' );
link.href = cfg.seite;
dsLink.href = DATENSCHUTZ;
link.target = '_blank';
dsLink.target = '_blank';
link.rel = 'noopener';
dsLink.rel = 'noopener';
link.textContent = 'Stattdessen direkt auf ' + cfg.host + ' ansehen';
dsLink.textContent = 'Datenschutzerklärung der ÖBB';
fuss.appendChild( link );
fuss.appendChild( dsLink );
box.appendChild( fuss );
box.appendChild( fuss );


container.appendChild( box );
container.appendChild( box );
function freigeben() {
if ( erledigt ) {
return;
}
erledigt = true;
lade( container, cfg );
}
wartende.push( freigeben );
knopf.addEventListener( 'click', function () {
if ( haken.checked ) {
setzeFreigabe( true );
gibAlleFrei();
} else {
freigeben();
}
} );
}
}


/* --- Einstieg ---------------------------------------------------------- */
/* --- Geladene Tafel ------------------------------------------------ */
function baueEinbettung( container ) {
function lade( container, cfg ) {
var $c = $( container );
container.textContent = '';
var roh = $.trim( String( $c.data( 'post' ) || '' ) );
 
var titel = document.createElement( 'div' );
titel.className = 'bahnsteiganzeiger-titel';
titel.textContent = titelText( cfg.typ );
container.appendChild( titel );
 
var rahmen = document.createElement( 'div' );
rahmen.className = 'bahnsteiganzeiger-rahmen';
rahmen.style.height = cfg.hoehe + 'px';
 
var iframe = document.createElement( 'iframe' );
iframe.className = 'bahnsteiganzeiger-iframe';
iframe.setAttribute( 'frameborder', '0' );
iframe.setAttribute( 'scrolling', 'auto' );
iframe.setAttribute( 'referrerpolicy', 'no-referrer' );
iframe.title = titelText( cfg.typ );
 
var url = baueUrl( cfg.eva, cfg.typ, cfg.anzahl );
var timer = null;


if ( !roh ) {
function laden() {
var instanz = $.trim( String( $c.data( 'instanz' ) || '' ) )
// Cache-Buster erzwingt frische Daten bei jedem Neuladen.
.replace( /^https?:\/\//i, '' ).replace( /\/+$/, '' );
iframe.src = url + '&_ts=' + Date.now();
var benutzer = $.trim( String( $c.data( 'benutzer' ) || '' ) )
}
.replace( /^@/, '' );
laden();
var id = $.trim( String( $c.data( 'id' ) || '' ) );
 
if ( instanz && benutzer && id ) {
rahmen.appendChild( iframe );
roh = 'https://' + instanz + '/@' + benutzer + '/' + id;
container.appendChild( rahmen );
 
if ( cfg.intervall > 0 ) {
var hinweis = document.createElement( 'div' );
hinweis.className = 'bahnsteiganzeiger-hinweis';
hinweis.textContent = 'Aktualisiert automatisch (alle ' +
cfg.intervall + ' s).';
container.appendChild( hinweis );
timer = setInterval( laden, cfg.intervall * 1000 );
}
 
var fuss = stil( document.createElement( 'div' ), {
fontSize: '85%',
color: '#54595d',
marginTop: '0.4em'
} );
var zurueck = document.createElement( 'a' );
zurueck.href = '#';
zurueck.textContent = 'Verbindung zur ÖBB wieder sperren';
zurueck.addEventListener( 'click', function ( e ) {
e.preventDefault();
if ( timer ) {
clearInterval( timer );
}
}
setzeFreigabe( false );
sperre( container, cfg );
} );
fuss.appendChild( zurueck );
container.appendChild( fuss );
}
/* --- Einstieg ------------------------------------------------------ */
function baueAnzeiger( container ) {
var eva = ( container.getAttribute( 'data-evaid' ) || '' )
.trim().replace( /^0+/, '' );
var typ = ( container.getAttribute( 'data-typ' ) || 'dep' ).toLowerCase();
if ( typ !== 'arr' ) {
typ = 'dep';
}
}


var adresse = leseAdresse( roh );
if ( !eva ) {
if ( !adresse ) {
container.textContent = 'Bahnsteiganzeiger: keine evaId angegeben.';
container.textContent = 'Mastodon-Einbettung: keine gültige ' +
container.className += ' bahnsteiganzeiger-fehler';
'Beitragsadresse angegeben (erwartet z. B. ' +
'https://mastodon.social/@benutzer/123456789012345678).';
return;
return;
}
}
var intervall = container.getAttribute( 'data-intervall' );
intervall = ( intervall === null ) ? STD_INTERVALL : parseInt( intervall, 10 );


var cfg = {
var cfg = {
host: adresse.host,
eva: eva,
seite: adresse.seite,
typ: typ,
embed: adresse.embed,
anzahl: parseInt( container.getAttribute( 'data-anzahl' ), 10 ) || STD_ANZAHL,
titel: $.trim( String( $c.data( 'titel' ) || '' ) ) || 'Mastodon-Beitrag',
hoehe: parseInt( container.getAttribute( 'data-hoehe' ), 10 ) || STD_HOEHE,
breite: masszahl( $c.data( 'width' ), '100%' ),
intervall: isFinite( intervall ) ? intervall : STD_INTERVALL
hoehe: masszahl( $c.data( 'height' ), STD_HOEHE + 'px' )
};
};


if ( String( $c.data( 'sperre' ) || '' ) === 'ja' ) {
if ( freigegeben() ) {
lade( container, cfg );
} else {
sperre( container, cfg );
sperre( container, cfg );
} else {
lade( container, cfg );
}
}
}
}


$( function () {
function init() {
$( '.mastodon-embed' ).each( function () {
var liste = document.querySelectorAll( '.bahnsteiganzeiger' );
Array.prototype.forEach.call( liste, function ( c ) {
try {
try {
baueEinbettung( this );
baueAnzeiger( c );
} catch ( e ) {
} catch ( e ) {
this.textContent = 'Mastodon-Beitrag konnte nicht geladen werden.';
c.textContent = 'Bahnsteiganzeiger konnte nicht geladen werden.';
if ( window.mw && mw.log ) {
if ( window.mw && mw.log ) {
mw.log.error( e );
mw.log.error( e );
Zeile 280: Zeile 306:
}
}
} );
} );
}
$( function () {
if ( document.querySelector( '.bahnsteiganzeiger' ) ) {
init();
}
} );
} );


}() );
}() );

Aktuelle Version vom 19. September 2026, 19:52 Uhr

/**
 * 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 einem einfachen Container – und kann es zusätzlich
 * alle 60 s neu laden (was ein rohes iframe nie könnte).
 *
 * ZWEI-KLICK-SPERRE
 * Die Tafel liegt auf fahrplan.oebb.at. Ein automatisch geladenes iframe
 * würde die IP-Adresse jedes Besuchers an die ÖBB übertragen, ohne dass er
 * das wollte. Deshalb erscheint zuerst eine Vorschaubox; erst der Klick baut
 * die Verbindung auf. Auf Wunsch merkt sich der Browser die Freigabe für die
 * laufende Sitzung (sessionStorage – kein Cookie, kein Banner); sie gilt dann
 * für alle ÖBB-Tafeln des Wikis und endet mit dem Schließen des Browsers.
 * Unter der geladenen Tafel steht ein Link zum Zurücknehmen.
 *
 * EINBINDUNG AUF EINER SEITE
 *   <div class="bahnsteiganzeiger" data-evaid="1231805" data-typ="dep"></div>
 *   <div class="bahnsteiganzeiger" data-evaid="1231805" data-typ="arr"></div>
 *
 * 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 808; bei mehr Zeilen als
 *                   hineinpassen scrollt die Tafel innen).
 *   data-intervall  Neulade-Intervall in Sekunden (Standard 60; 0 = aus).
 * ============================================================================
 */
( function () {
	'use strict';

	var BASIS = 'https://fahrplan.oebb.at/bin/stboard.exe/dn';
	var LIVETICKER = 'vs_scotty.vs_liveticker';
	var PRODUCTS = '1111111111111'; // alle Verkehrsmittel inkl. Bus
	var STD_ANZAHL = 40;
	var STD_HOEHE = 808;
	var STD_INTERVALL = 60; // Sekunden

	/* --- Zwei-Klick-Sperre ------------------------------------------- */
	var SPEICHER = 'wiki-freigabe-oebb';
	var DATENSCHUTZ = 'https://www.oebb.at/de/datenschutz';

	/* Noch gesperrte Tafeln dieser Seite – damit ein "merken"-Klick auch
	 * die zweite Tafel (Ankünfte) gleich mitfreigibt. */
	var wartende = [];

	function freigegeben() {
		try {
			return window.sessionStorage.getItem( SPEICHER ) === 'ja';
		} catch ( e ) {
			return false; // privater Modus o.Ä.: dann eben jedes Mal fragen
		}
	}

	function setzeFreigabe( ja ) {
		try {
			if ( ja ) {
				window.sessionStorage.setItem( SPEICHER, 'ja' );
			} else {
				window.sessionStorage.removeItem( SPEICHER );
			}
		} catch ( e ) {}
	}

	function gibAlleFrei() {
		var liste = wartende.slice();
		wartende.length = 0;
		liste.forEach( function ( fn ) {
			fn();
		} );
	}

	function stil( el, regeln ) {
		var name;
		for ( name in regeln ) {
			el.style[ name ] = regeln[ name ];
		}
		return el;
	}

	/* --- ÖBB-Adressen ------------------------------------------------- */
	function baueUrl( eva, typ, anzahl ) {
		return BASIS +
			'?L=' + LIVETICKER +
			'&evaId=' + encodeURIComponent( eva ) +
			'&boardType=' + typ +
			'&productsFilter=' + PRODUCTS +
			'&dirInput=' +
			'&tickerID=' + typ +
			'&start=yes' +
			'&eqstops=true' +
			'&showJourneys=' + anzahl +
			'&additionalTime=0';
	}

	function titelText( typ ) {
		return typ === 'arr' ? 'Ankünfte' : 'Abfahrten';
	}

	/* --- Vorschaubox (noch nichts geladen) ---------------------------- */
	function sperre( container, cfg ) {
		var erledigt = false;

		container.textContent = '';
		container.className = container.className
			.replace( / ?bahnsteiganzeiger-fehler/, '' );

		var box = stil( document.createElement( 'div' ), {
			border: '1px solid #a2a9b1',
			borderRadius: '2px',
			background: '#f8f9fa',
			padding: '1em',
			maxWidth: '42em',
			boxSizing: 'border-box'
		} );
		box.className = 'bahnsteiganzeiger-sperre';

		var kopf = stil( document.createElement( 'div' ), {
			fontWeight: 'bold',
			fontSize: '108%',
			marginBottom: '0.4em'
		} );
		kopf.textContent = titelText( cfg.typ ) + ' – Live-Tafel der ÖBB';
		box.appendChild( kopf );

		var text = stil( document.createElement( 'p' ), { margin: '0.4em 0' } );
		text.textContent = 'Die Tafel liegt auf fahrplan.oebb.at und gehört der ' +
			'ÖBB-Personenverkehr AG. Beim Laden erfährt die ÖBB deine ' +
			'IP-Adresse, welchen Bahnhof du gerade ansiehst und technische ' +
			'Angaben zu deinem Gerät und Browser. Solange du nicht klickst, ' +
			'verlässt keine einzige Anfrage dieses Wiki.';
		box.appendChild( text );

		var knopf = stil( document.createElement( 'button' ), {
			cursor: 'pointer',
			padding: '0.4em 0.9em',
			fontWeight: 'bold'
		} );
		knopf.type = 'button';
		knopf.textContent = titelText( cfg.typ ) + ' laden';

		var haken = document.createElement( 'input' );
		haken.type = 'checkbox';

		var hakenLabel = stil( document.createElement( 'label' ), {
			display: 'block',
			margin: '0.7em 0',
			fontSize: '92%'
		} );
		hakenLabel.appendChild( haken );
		hakenLabel.appendChild( document.createTextNode(
			' Für diese Sitzung merken – gilt dann für alle ÖBB-Tafeln im ' +
			'Wiki und endet, sobald du den Browser schließt. Es wird kein ' +
			'Cookie gesetzt.'
		) );
		box.appendChild( hakenLabel );

		box.appendChild( stil( document.createElement( 'div' ),
			{ margin: '0.7em 0' } ) ).appendChild( knopf );

		var fuss = stil( document.createElement( 'div' ), {
			fontSize: '85%',
			color: '#54595d',
			marginTop: '0.6em'
		} );
		var dsLink = document.createElement( 'a' );
		dsLink.href = DATENSCHUTZ;
		dsLink.target = '_blank';
		dsLink.rel = 'noopener';
		dsLink.textContent = 'Datenschutzerklärung der ÖBB';
		fuss.appendChild( dsLink );
		box.appendChild( fuss );

		container.appendChild( box );

		function freigeben() {
			if ( erledigt ) {
				return;
			}
			erledigt = true;
			lade( container, cfg );
		}

		wartende.push( freigeben );

		knopf.addEventListener( 'click', function () {
			if ( haken.checked ) {
				setzeFreigabe( true );
				gibAlleFrei();
			} else {
				freigeben();
			}
		} );
	}

	/* --- Geladene Tafel ------------------------------------------------ */
	function lade( container, cfg ) {
		container.textContent = '';

		var titel = document.createElement( 'div' );
		titel.className = 'bahnsteiganzeiger-titel';
		titel.textContent = titelText( cfg.typ );
		container.appendChild( titel );

		var rahmen = document.createElement( 'div' );
		rahmen.className = 'bahnsteiganzeiger-rahmen';
		rahmen.style.height = cfg.hoehe + 'px';

		var iframe = document.createElement( 'iframe' );
		iframe.className = 'bahnsteiganzeiger-iframe';
		iframe.setAttribute( 'frameborder', '0' );
		iframe.setAttribute( 'scrolling', 'auto' );
		iframe.setAttribute( 'referrerpolicy', 'no-referrer' );
		iframe.title = titelText( cfg.typ );

		var url = baueUrl( cfg.eva, cfg.typ, cfg.anzahl );
		var timer = null;

		function laden() {
			// Cache-Buster erzwingt frische Daten bei jedem Neuladen.
			iframe.src = url + '&_ts=' + Date.now();
		}
		laden();

		rahmen.appendChild( iframe );
		container.appendChild( rahmen );

		if ( cfg.intervall > 0 ) {
			var hinweis = document.createElement( 'div' );
			hinweis.className = 'bahnsteiganzeiger-hinweis';
			hinweis.textContent = 'Aktualisiert automatisch (alle ' +
				cfg.intervall + ' s).';
			container.appendChild( hinweis );
			timer = setInterval( laden, cfg.intervall * 1000 );
		}

		var fuss = stil( document.createElement( 'div' ), {
			fontSize: '85%',
			color: '#54595d',
			marginTop: '0.4em'
		} );
		var zurueck = document.createElement( 'a' );
		zurueck.href = '#';
		zurueck.textContent = 'Verbindung zur ÖBB wieder sperren';
		zurueck.addEventListener( 'click', function ( e ) {
			e.preventDefault();
			if ( timer ) {
				clearInterval( timer );
			}
			setzeFreigabe( false );
			sperre( container, cfg );
		} );
		fuss.appendChild( zurueck );
		container.appendChild( fuss );
	}

	/* --- Einstieg ------------------------------------------------------ */
	function baueAnzeiger( container ) {
		var eva = ( container.getAttribute( 'data-evaid' ) || '' )
			.trim().replace( /^0+/, '' );
		var typ = ( container.getAttribute( 'data-typ' ) || 'dep' ).toLowerCase();
		if ( typ !== 'arr' ) {
			typ = 'dep';
		}

		if ( !eva ) {
			container.textContent = 'Bahnsteiganzeiger: keine evaId angegeben.';
			container.className += ' bahnsteiganzeiger-fehler';
			return;
		}

		var intervall = container.getAttribute( 'data-intervall' );
		intervall = ( intervall === null ) ? STD_INTERVALL : parseInt( intervall, 10 );

		var cfg = {
			eva: eva,
			typ: typ,
			anzahl: parseInt( container.getAttribute( 'data-anzahl' ), 10 ) || STD_ANZAHL,
			hoehe: parseInt( container.getAttribute( 'data-hoehe' ), 10 ) || STD_HOEHE,
			intervall: isFinite( intervall ) ? intervall : STD_INTERVALL
		};

		if ( freigegeben() ) {
			lade( container, cfg );
		} else {
			sperre( container, cfg );
		}
	}

	function init() {
		var liste = document.querySelectorAll( '.bahnsteiganzeiger' );
		Array.prototype.forEach.call( liste, function ( c ) {
			try {
				baueAnzeiger( c );
			} catch ( e ) {
				c.textContent = 'Bahnsteiganzeiger konnte nicht geladen werden.';
				if ( window.mw && mw.log ) {
					mw.log.error( e );
				}
			}
		} );
	}

	$( function () {
		if ( document.querySelector( '.bahnsteiganzeiger' ) ) {
			init();
		}
	} );

}() );