MediaWiki:Gadget-Wetter.js
Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.
- Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
- Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
- Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
/**
* MediaWiki:Gadget-Wetter.js
* =====================================================================
* Wetterwidget für Gemeindeseiten: zeigt aktuelle Messwerte der
* eingetragenen GeoSphere-Wetterstation sowie eine 3-Tages-Vorhersage
* (AROME-Modell, 2,5-km-Gitter) für die Koordinaten des Ortes.
*
* Datenquelle: GeoSphere Austria Dataset-API (Open Data, CC BY 4.0),
* kein API-Schlüssel nötig. Doku: https://dataset.api.hub.geosphere.at/v1/docs/
*
* EINRICHTUNG (einmalig):
* 1) Diesen Code nach [[MediaWiki:Gadget-Wetter.js]] kopieren.
* 2) In [[MediaWiki:Gadgets-definition]] ergänzen:
* * Wetter[ResourceLoader|default|hidden]|Wetter.js
* 3) [[MediaWiki:Gadget-Wetter]] anlegen, Inhalt z. B.:
* Wetterwidget mit GeoSphere-Daten.
* 4) [[Vorlage:Wetterwidget]] mit dem Inhalt der mitgelieferten Datei
* Vorlage-Wetterwidget.wikitext anlegen.
*
* VERWENDUNG:
* Auf der Gemeindeseite den {{Wetterwidget|...}}-Baustein einfügen –
* am einfachsten erzeugt über die Generator-Seite (Formular). Das Gadget
* findet alle Bausteine der Seite und befüllt sie beim Seitenaufruf.
*
* TECHNISCHE HINWEISE:
* - Pro Widget und Seitenaufruf entstehen zwei API-Anfragen (Messwerte,
* Vorhersage); das GeoSphere-Limit von 240 Anfragen pro Stunde gilt je
* Besucher und wird damit bei weitem nicht erreicht.
* - Die Parameternamen der Vorhersage werden mit Ausweichvarianten
* angefragt, da der Vorhersage-Endpunkt laut GeoSphere noch als
* "prerelease" gilt. Das AROME-Modell rechnet alle 3 Stunden neu und
* liefert 60 Stunden Vorhersage; laut GeoSphere wird der Datensatz
* Anfang 2027 durch einen Nachfolger (neuer Endpunkt) abgelöst.
* =====================================================================
*/
( function () {
'use strict';
var BASIS = 'https://dataset.api.hub.geosphere.at/v1';
var ZEITZONE = 'Europe/Vienna';
var MAX_TAGE = 3;
/* Parametersätze in absteigender Wunschreihenfolge. Antwortet der
* Server mit Status 400 (unbekannter Parameter), wird der nächste,
* kleinere Satz versucht. */
var MESS_SAETZE = [
[ 'TL', 'RF', 'DD', 'FFAM', 'FFX', 'RR', 'P0', 'SH' ],
[ 'TL', 'RF', 'DD', 'FFAM', 'RR' ],
[ 'TL' ]
];
var PROGNOSE_SAETZE = [
[ 't2m', 'rr_acc' ],
[ 't2m', 'rr' ],
[ 't2m' ]
];
$( function () {
$( '.wetterwidget' ).each( function () {
starteWidget( $( this ) );
} );
} );
/* ------------------------------------------------------------------
* Ablaufsteuerung je Widget
* ------------------------------------------------------------------ */
function starteWidget( $box ) {
var cfg = {
ort: $.trim( $box.attr( 'data-ort' ) || '' ),
latText: $.trim( $box.attr( 'data-lat' ) || '' ),
lonText: $.trim( $box.attr( 'data-lon' ) || '' ),
station: $.trim( $box.attr( 'data-station' ) || '' ),
stationsname: $.trim( $box.attr( 'data-stationsname' ) || '' ),
stationshoehe: $.trim( $box.attr( 'data-stationshoehe' ) || '' )
};
var latOk = isFinite( parseFloat( cfg.latText ) );
var lonOk = isFinite( parseFloat( cfg.lonText ) );
cfg.mitMesswerten = cfg.station !== '';
cfg.mitVorhersage = latOk && lonOk;
gestalteBox( $box );
$box.empty().append(
titelZeile( cfg ),
$( '<div>' ).css( kleinGrau() ).text( 'Wetterdaten werden geladen …' )
);
if ( !cfg.mitMesswerten && !cfg.mitVorhersage ) {
zeichneWidget( $box, cfg, {
messFehler: 'Im Baustein fehlen Stations-ID und Koordinaten – ' +
'bitte über die Generator-Seite neu erstellen.',
prognoseFehler: ''
} );
return;
}
var stand = { mess: null, messFehler: '', prognose: null, prognoseFehler: '' };
var offen = ( cfg.mitMesswerten ? 1 : 0 ) + ( cfg.mitVorhersage ? 1 : 0 );
function fertigMelden() {
offen -= 1;
if ( offen <= 0 ) {
zeichneWidget( $box, cfg, stand );
}
}
if ( cfg.mitMesswerten ) {
ladeMitVarianten( MESS_SAETZE, function ( satz ) {
return BASIS + '/station/current/tawes-v1-10min' +
'?parameters=' + satz.join( ',' ) +
'&station_ids=' + encodeURIComponent( cfg.station );
}, function ( fehler, antwort, satz ) {
if ( fehler ) {
stand.messFehler = fehler;
} else {
stand.mess = leseMesswerte( antwort, satz );
if ( !stand.mess ) {
stand.messFehler = 'Die Station liefert derzeit keine Werte.';
}
}
fertigMelden();
} );
}
if ( cfg.mitVorhersage ) {
ladeMitVarianten( PROGNOSE_SAETZE, function ( satz ) {
return BASIS + '/timeseries/forecast/nwp-v1-1h-2500m' +
'?parameters=' + satz.join( ',' ) +
'&lat_lon=' + encodeURIComponent( cfg.latText + ',' + cfg.lonText );
}, function ( fehler, antwort, satz ) {
if ( fehler ) {
stand.prognoseFehler = fehler;
} else {
stand.prognose = leseVorhersage( antwort, satz );
if ( !stand.prognose ) {
stand.prognoseFehler = 'Die Vorhersagedaten waren unvollständig.';
}
}
fertigMelden();
} );
}
}
/* Fragt der Reihe nach die Parametersätze an; bei Status 400 wird der
* nächste Satz versucht (falls sich Parameternamen der API ändern). */
function ladeMitVarianten( saetze, baueUrl, fertig ) {
var i = 0;
function versuch() {
$.getJSON( baueUrl( saetze[ i ] ) )
.done( function ( antwort ) {
fertig( null, antwort, saetze[ i ] );
} )
.fail( function ( x ) {
if ( x && x.status === 400 && i + 1 < saetze.length ) {
i += 1;
versuch();
return;
}
fertig( netzFehlerText( x ), null, null );
} );
}
versuch();
}
function netzFehlerText( x ) {
if ( !x || x.status === 0 ) {
return 'Der Browser hat die Anfrage blockiert (etwa wegen CORS) ' +
'oder es besteht keine Internetverbindung.';
}
return 'Der GeoSphere-Server antwortete mit Status ' + x.status + '.';
}
/* ------------------------------------------------------------------
* API-Antworten auswerten
* ------------------------------------------------------------------ */
/* Liefert das Parameterobjekt (mit data/unit) aus einer GeoJSON-
* Antwort, tolerant gegenüber Gross-/Kleinschreibung und Struktur. */
function holeParameter( antwort, name ) {
var feature = antwort && antwort.features && antwort.features[ 0 ];
var params = feature && feature.properties && feature.properties.parameters;
var schluessel, i;
if ( !params ) {
return null;
}
if ( $.isArray( params ) ) {
for ( i = 0; i < params.length; i++ ) {
if ( params[ i ] && String( params[ i ].name ).toLowerCase() ===
name.toLowerCase() ) {
return params[ i ];
}
}
return null;
}
if ( params[ name ] ) {
return params[ name ];
}
for ( schluessel in params ) {
if ( schluessel.toLowerCase() === name.toLowerCase() ) {
return params[ schluessel ];
}
}
return null;
}
function holeZeiten( antwort ) {
var roh = ( antwort && antwort.timestamps ) || [];
var zeiten = [];
var i, d;
for ( i = 0; i < roh.length; i++ ) {
d = new Date( roh[ i ] );
zeiten.push( isFinite( d.getTime() ) ? d : null );
}
return zeiten;
}
function zahlOderNull( wert ) {
var z = parseFloat( wert );
return isFinite( z ) ? z : null;
}
/* Aktuelle Messwerte: jeweils der letzte Eintrag der Zeitreihe. */
function leseMesswerte( antwort, satz ) {
var zeiten = holeZeiten( antwort );
var idx = zeiten.length - 1;
var werte = {};
var gefunden = false;
var i, p, wert;
if ( idx < 0 ) {
return null;
}
for ( i = 0; i < satz.length; i++ ) {
p = holeParameter( antwort, satz[ i ] );
wert = ( p && p.data ) ? zahlOderNull( p.data[ idx ] ) : null;
werte[ satz[ i ] ] = wert;
if ( wert !== null ) {
gefunden = true;
}
}
return gefunden ? { werte: werte, zeit: zeiten[ idx ] } : null;
}
/* Vorhersage: stündliche Werte zu Tagen (Wiener Zeit) bündeln. */
function leseVorhersage( antwort, satz ) {
var zeiten = holeZeiten( antwort );
var temperatur = holeParameter( antwort, 't2m' );
var tDaten = temperatur && temperatur.data;
var regenName = $.inArray( 'rr_acc', satz ) !== -1 ? 'rr_acc' :
( $.inArray( 'rr', satz ) !== -1 ? 'rr' : null );
var kumuliert = regenName === 'rr_acc';
var regen = regenName ? holeParameter( antwort, regenName ) : null;
var rDaten = regen && regen.data;
var inKelvin = false;
var proTag = {};
var reihenfolge = [];
var ab = Date.now() - 45 * 60000; /* laufende Stunde noch mitnehmen */
var i, t, r, vorher, schluessel, eintrag, tage, tag;
if ( !tDaten || !tDaten.length || !zeiten.length ) {
return null;
}
inKelvin = istKelvin( temperatur, tDaten );
for ( i = 0; i < zeiten.length && i < tDaten.length; i++ ) {
if ( !zeiten[ i ] ) {
continue;
}
/* Niederschlag der Stunde: bei kumulierten Werten Differenz
* zum vorigen Zeitschritt, sonst der Wert selbst. */
r = null;
if ( rDaten ) {
if ( kumuliert ) {
vorher = i > 0 ? zahlOderNull( rDaten[ i - 1 ] ) : 0;
r = zahlOderNull( rDaten[ i ] );
r = ( r === null || vorher === null ) ? null :
Math.max( 0, r - vorher );
} else {
r = zahlOderNull( rDaten[ i ] );
}
}
if ( zeiten[ i ].getTime() < ab ) {
continue; /* Vergangenheit erst nach der Differenzbildung verwerfen */
}
t = zahlOderNull( tDaten[ i ] );
if ( t !== null && inKelvin ) {
t -= 273.15;
}
schluessel = tagesSchluessel( zeiten[ i ] );
if ( !proTag[ schluessel ] ) {
proTag[ schluessel ] = {
ersteZeit: zeiten[ i ], min: null, max: null,
regen: null, stunden: 0
};
reihenfolge.push( schluessel );
}
eintrag = proTag[ schluessel ];
eintrag.stunden += 1;
if ( t !== null ) {
eintrag.min = ( eintrag.min === null ) ? t : Math.min( eintrag.min, t );
eintrag.max = ( eintrag.max === null ) ? t : Math.max( eintrag.max, t );
}
if ( r !== null ) {
eintrag.regen = ( eintrag.regen === null ) ? r : eintrag.regen + r;
}
}
tage = [];
for ( i = 0; i < reihenfolge.length && tage.length < MAX_TAGE; i++ ) {
tag = proTag[ reihenfolge[ i ] ];
if ( tag.stunden >= 3 && tag.min !== null ) {
tage.push( {
name: tagesName( tag.ersteZeit, reihenfolge[ i ] ),
min: tag.min,
max: tag.max,
regen: tag.regen
} );
}
}
return tage.length ? { tage: tage } : null;
}
function istKelvin( parameter, daten ) {
var einheit = parameter && parameter.unit ? String( parameter.unit ) : '';
var i, w;
if ( /^k(elvin)?$/i.test( $.trim( einheit ) ) ) {
return true;
}
if ( /c/i.test( einheit ) ) {
return false;
}
for ( i = 0; i < daten.length; i++ ) {
w = zahlOderNull( daten[ i ] );
if ( w !== null ) {
return w > 150; /* plausible Celsiuswerte liegen weit darunter */
}
}
return false;
}
function tagesSchluessel( datum ) {
try {
return datum.toLocaleDateString( 'de-AT', { timeZone: ZEITZONE } );
} catch ( e ) {
return datum.toDateString();
}
}
function tagesName( datum, schluessel ) {
var heute = tagesSchluessel( new Date() );
var morgen = tagesSchluessel( new Date( Date.now() + 86400000 ) );
if ( schluessel === heute ) {
return 'Heute';
}
if ( schluessel === morgen ) {
return 'Morgen';
}
try {
return datum.toLocaleDateString( 'de-AT',
{ weekday: 'long', timeZone: ZEITZONE } );
} catch ( e ) {
return schluessel;
}
}
function uhrzeit( datum ) {
try {
return datum.toLocaleTimeString( 'de-AT',
{ hour: '2-digit', minute: '2-digit', timeZone: ZEITZONE } );
} catch ( e ) {
return '';
}
}
/* ------------------------------------------------------------------
* Darstellung
* ------------------------------------------------------------------ */
function zeichneWidget( $box, cfg, stand ) {
$box.empty().append( titelZeile( cfg ) );
/* --- Abschnitt: aktuelle Messwerte --- */
if ( stand.mess ) {
$box.append( messwerteBlock( cfg, stand.mess ) );
} else if ( cfg.mitMesswerten || stand.messFehler ) {
$box.append( fehlerZeile( 'Aktuelle Messwerte nicht verfügbar: ' +
stand.messFehler ) );
}
/* --- Abschnitt: Vorhersage --- */
if ( stand.prognose ) {
$box.append( trennlinie(), vorhersageBlock( stand.prognose ) );
} else if ( cfg.mitVorhersage ) {
$box.append( trennlinie(), fehlerZeile( 'Vorhersage nicht verfügbar: ' +
stand.prognoseFehler ) );
}
$box.append(
$( '<div>' )
.css( { 'margin-top': '0.6em', 'font-size': '80%', color: '#54595d' } )
.text( 'Datenquelle: GeoSphere Austria (CC BY 4.0)' +
( stand.prognose ? ' · Vorhersage: AROME-Modell' : '' ) )
);
}
function messwerteBlock( cfg, mess ) {
var w = mess.werte;
var $block = $( '<div>' );
var zeilen = [];
var windKmh, boeenKmh, stationsZeile;
if ( w.TL !== null && w.TL !== undefined ) {
$block.append(
$( '<div>' )
.css( { 'font-size': '2.1em', 'font-weight': 'bold',
'line-height': '1.15' } )
.text( formatZahl( w.TL, 1 ) + ' °C' )
);
}
if ( istZahl( w.FFAM ) ) {
windKmh = Math.round( w.FFAM * 3.6 );
boeenKmh = istZahl( w.FFX ) ? Math.round( w.FFX * 3.6 ) : null;
zeilen.push( 'Wind: ' + windKmh + ' km/h' +
( istZahl( w.DD ) ? ' aus ' + windrichtung( w.DD ) : '' ) +
( boeenKmh !== null && boeenKmh > windKmh + 5 ?
', Böen ' + boeenKmh + ' km/h' : '' ) );
}
if ( istZahl( w.RF ) ) {
zeilen.push( 'Luftfeuchtigkeit: ' + Math.round( w.RF ) + ' %' );
}
if ( istZahl( w.RR ) && w.RR >= 0.05 ) {
zeilen.push( 'Niederschlag: ' + formatZahl( w.RR, 1 ) +
' mm in 10 Minuten' );
}
if ( istZahl( w.P0 ) ) {
zeilen.push( 'Luftdruck: ' + Math.round( w.P0 ) + ' hPa' );
}
if ( istZahl( w.SH ) && w.SH >= 1 ) {
zeilen.push( 'Schneehöhe: ' + Math.round( w.SH ) + ' cm' );
}
$.each( zeilen, function ( i, text ) {
$block.append( $( '<div>' ).css( { 'font-size': '92%',
margin: '0.15em 0' } ).text( text ) );
} );
stationsZeile = 'Station ' + ( cfg.stationsname || 'Nr. ' + cfg.station );
if ( cfg.stationshoehe ) {
stationsZeile += ', ' + cfg.stationshoehe + ' m';
}
if ( mess.zeit ) {
stationsZeile = 'Gemessen um ' + uhrzeit( mess.zeit ) + ' Uhr · ' +
stationsZeile;
}
$block.append( $( '<div>' ).css( kleinGrau() ).text( stationsZeile ) );
return $block;
}
function vorhersageBlock( prognose ) {
var $block = $( '<div>' );
var $reihe = $( '<div>' ).css( { display: 'flex', 'flex-wrap': 'wrap' } );
$block.append(
$( '<div>' )
.css( { 'font-weight': 'bold', 'font-size': '92%',
'margin-bottom': '0.3em' } )
.text( 'Vorhersage (Höchst- / Tiefstwert)' )
);
$.each( prognose.tage, function ( i, tag ) {
var regenText = ( tag.regen === null ) ? '' :
( tag.regen < 0.05 ? 'trocken' :
formatZahl( tag.regen, 1 ) + ' mm' );
var $zelle = $( '<div>' ).css( { flex: '1 1 6em',
'min-width': '6em', margin: '0.15em 0' } );
$zelle.append(
$( '<div>' ).css( { 'font-size': '88%', 'font-weight': 'bold' } )
.text( tag.name ),
$( '<div>' ).text( Math.round( tag.max ) + '° / ' +
Math.round( tag.min ) + '°' )
);
if ( regenText ) {
$zelle.append( $( '<div>' ).css( { 'font-size': '85%',
color: '#54595d' } ).text( regenText ) );
}
$reihe.append( $zelle );
} );
$block.append( $reihe );
return $block;
}
function titelZeile( cfg ) {
return $( '<div>' )
.css( { 'font-weight': 'bold', 'font-size': '108%',
'margin-bottom': '0.35em' } )
.text( 'Wetter in ' + ( cfg.ort || 'der Region' ) );
}
function trennlinie() {
return $( '<div>' ).css( { 'border-top': '1px solid #c8ccd1',
margin: '0.6em 0' } );
}
function fehlerZeile( text ) {
return $( '<div>' ).css( { 'font-size': '88%', color: '#b32424',
margin: '0.3em 0' } ).text( text );
}
function gestalteBox( $box ) {
$box.css( {
border: '1px solid #a2a9b1',
'border-radius': '2px',
background: '#f8f9fa',
padding: '0.8em 1em',
'max-width': '26em',
'box-sizing': 'border-box'
} );
}
function kleinGrau() {
return { 'font-size': '85%', color: '#54595d', 'margin-top': '0.35em' };
}
/* ------------------------------------------------------------------
* Kleine Helfer
* ------------------------------------------------------------------ */
function istZahl( wert ) {
return typeof wert === 'number' && isFinite( wert );
}
function formatZahl( wert, nachkomma ) {
return wert.toFixed( nachkomma ).replace( '.', ',' ).replace( '-0,0', '0,0' );
}
function windrichtung( grad ) {
var richtungen = [ 'Nord', 'Nordost', 'Ost', 'Südost',
'Süd', 'Südwest', 'West', 'Nordwest' ];
return richtungen[ Math.round( grad / 45 ) % 8 ];
}
}() );