// ==UserScript==
// @name WME NINA Warnungen Beta
// @namespace https://gf.qytechs.cn/de/users/863740-horst-wittlich
// @version 2025.08.20
// @description Zeigt NINA Warnmeldungen im Waze Map Editor Scripts-Tab an
// @author Hiwi234
// @match https://www.waze.com/editor*
// @match https://www.waze.com/*/editor*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// CSS Styles hinzufügen
GM_addStyle(`
.nina-tab-content {
padding: 10px;
font-family: Arial, sans-serif;
height: 100%;
overflow-y: auto;
}
.nina-header {
background: #ff6b35;
color: white;
padding: 10px;
margin: -10px -10px 10px -10px;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
}
.nina-stats {
background: #f8f9fa;
padding: 8px;
border-radius: 4px;
margin-bottom: 10px;
font-size: 0.9em;
color: #495057;
}
.nina-controls {
background: #f8f9fa;
padding: 8px;
border-radius: 4px;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.nina-checkbox {
display: flex;
align-items: center;
gap: 5px;
font-size: 0.9em;
}
.nina-checkbox input[type="checkbox"] {
margin: 0;
}
.nina-warning {
border-left: 4px solid #ff6b35;
padding: 8px;
margin-bottom: 10px;
background: #fff5f2;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s ease;
}
.nina-warning:hover {
background: #ffe8e0;
transform: translateX(2px);
box-shadow: 2px 2px 8px rgba(0,0,0,0.1);
}
.nina-warning-title {
font-weight: bold;
color: #d63031;
margin-bottom: 4px;
font-size: 1.1em;
}
.nina-warning-meta {
color: #636e72;
font-size: 0.9em;
margin-bottom: 6px;
line-height: 1.4;
}
.nina-warning-type {
background: #ff6b35;
color: white;
padding: 2px 6px;
border-radius: 12px;
font-size: 0.8em;
display: inline-block;
margin-right: 4px;
}
.nina-refresh {
cursor: pointer;
margin-right: 10px;
padding: 4px 8px;
background: rgba(255,255,255,0.2);
border-radius: 4px;
transition: background 0.2s;
}
.nina-refresh:hover {
background: rgba(255,255,255,0.3);
}
.nina-loading {
text-align: center;
padding: 20px;
color: #636e72;
}
.nina-empty {
text-align: center;
padding: 30px;
color: #636e72;
background: #f8f9fa;
border-radius: 8px;
margin: 20px 0;
}
.nina-source-badge {
background: #6c757d;
color: white;
padding: 1px 4px;
border-radius: 2px;
font-size: 0.7em;
margin-left: 4px;
}
.nina-goto-btn {
float: right;
background: #007bff;
color: white;
border: none;
padding: 2px 6px;
border-radius: 3px;
font-size: 0.8em;
cursor: pointer;
margin-left: 8px;
}
.nina-goto-btn:hover {
background: #0056b3;
}
`);
let ninaWarnings = [];
let ninaMarkers = [];
let ninaTab = null;
let tabPane = null;
let showMarkersOnMap = true; // Standard: Icons anzeigen
function initNINAScript() {
try {
// Scripts-Tab registrieren
const { tabLabel, tabPane: pane } = W.userscripts.registerSidebarTab("nina-warnings");
tabPane = pane;
// Tab-Label setzen
tabLabel.innerHTML = 'NINA';
tabLabel.title = 'NINA Warnmeldungen';
tabLabel.style.fontSize = '16px';
// Warten bis Tab im DOM ist
W.userscripts.waitForElementConnected(tabPane).then(() => {
setupTabContent();
loadNINAWarnings();
// Alle 5 Minuten aktualisieren
setInterval(loadNINAWarnings, 300000);
});
} catch (error) {
console.error('Fehler beim Registrieren des NINA Tabs:', error);
}
}
function setupTabContent() {
tabPane.innerHTML = `
<div class="nina-tab-content">
<div class="nina-header">
<span>🔔 NINA Warnungen</span>
<span class="nina-refresh" id="nina-refresh-btn">🔄 Aktualisieren</span>
</div>
<div class="nina-stats" id="nina-stats">
Lade Warnungen...
</div>
<div class="nina-controls">
<div class="nina-checkbox">
<input type="checkbox" id="show-markers-checkbox" checked>
<label for="show-markers-checkbox">📍 Icons auf Karte anzeigen</label>
</div>
</div>
<div id="nina-warnings-content" class="nina-loading">
<div>📡 Verbinde mit NINA API...</div>
</div>
</div>
`;
// Event Listener hinzufügen
const refreshBtn = document.getElementById('nina-refresh-btn');
if (refreshBtn) {
refreshBtn.addEventListener('click', loadNINAWarnings);
}
const showMarkersCheckbox = document.getElementById('show-markers-checkbox');
if (showMarkersCheckbox) {
showMarkersCheckbox.addEventListener('change', toggleMapMarkers);
}
}
function loadNINAWarnings() {
console.log('Lade NINA Warnungen...');
updateStats('Lade Warnungen...');
// Alle verfügbaren Warndienste laden
const sources = [
'mowas',
'biwapp',
'katwarn',
'dwd',
'lhp'
];
let allWarnings = [];
let loadedCount = 0;
sources.forEach(source => {
GM_xmlhttpRequest({
method: 'GET',
url: `https://warnung.bund.de/api31/${source}/mapData.json`,
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
if (data && Array.isArray(data) && data.length > 0) {
console.log(`Gefunden ${data.length} Warnungen von ${source}`);
// Quelle zu jeder Warnung hinzufügen
data.forEach(warning => {
warning.source = source;
});
allWarnings = allWarnings.concat(data);
}
} catch (e) {
console.warn(`Fehler beim Laden von ${source}:`, e);
}
loadedCount++;
if (loadedCount === sources.length) {
console.log(`Insgesamt ${allWarnings.length} Warnungen geladen`);
processWarnings(allWarnings);
}
},
onerror: function(error) {
console.error(`Fehler beim Laden von ${source}:`, error);
loadedCount++;
if (loadedCount === sources.length) {
console.log(`Insgesamt ${allWarnings.length} Warnungen geladen`);
processWarnings(allWarnings);
}
}
});
});
}
function processWarnings(warnings) {
console.log('Verarbeite', warnings.length, 'Warnungen');
// Sortierung: Neueste zuerst (nach startDate oder falls nicht vorhanden nach ID)
warnings.sort((a, b) => {
const dateA = a.startDate ? new Date(a.startDate) : new Date(0);
const dateB = b.startDate ? new Date(b.startDate) : new Date(0);
return dateB - dateA; // Neueste zuerst
});
ninaWarnings = warnings;
// Alte Marker entfernen
clearMarkers();
// Neue Marker hinzufügen (nur wenn Checkbox aktiviert)
if (showMarkersOnMap) {
addMarkersToMap(warnings);
}
// Tab aktualisieren
updateTabContent(warnings);
}
function clearMarkers() {
ninaMarkers.forEach(markerData => {
if (markerData.layer && markerData.marker) {
try {
markerData.layer.removeMarker(markerData.marker);
} catch (e) {
console.warn('Fehler beim Entfernen des Markers:', e);
}
}
});
ninaMarkers = [];
// Falls Layer komplett neu erstellt werden soll
const existingLayer = W.map.getOLMap().getLayersByName('ninaLayer')[0];
if (existingLayer) {
try {
existingLayer.clearMarkers();
} catch (e) {
console.warn('clearMarkers nicht verfügbar, verwende destroy:', e);
try {
W.map.getOLMap().removeLayer(existingLayer);
} catch (e2) {
console.warn('Layer konnte nicht entfernt werden:', e2);
}
}
}
}
function addMarkersToMap(warnings) {
// Nur hinzufügen wenn Checkbox aktiviert ist
if (!showMarkersOnMap) {
console.log('Marker-Anzeige deaktiviert, überspringe Marker-Erstellung');
return;
}
// NINA Layer erstellen oder wiederverwenden
let ninaLayer = W.map.getOLMap().getLayersByName('ninaLayer')[0];
if (!ninaLayer) {
ninaLayer = new OpenLayers.Layer.Markers('ninaLayer');
W.map.getOLMap().addLayer(ninaLayer);
console.log('NINA Layer erstellt');
}
console.log(`Füge ${warnings.length} Marker hinzu`);
warnings.forEach((warning, index) => {
// Deutsche Städte für Demo-Zwecke
const demoLocations = [
[52.5200, 13.4050], // Berlin
[48.1351, 11.5820], // München
[53.5511, 9.9937], // Hamburg
[50.9375, 6.9603], // Köln
[50.1109, 8.6821], // Frankfurt
[48.7758, 9.1829], // Stuttgart
[51.2277, 6.7735], // Düsseldorf
[51.0504, 13.7373], // Dresden
[53.0793, 8.8017], // Bremen
[52.3759, 9.7320], // Hannover
[49.4521, 11.0767], // Nürnberg
[51.3127, 9.4797], // Kassel
[54.0924, 13.2015], // Rostock
[49.0069, 8.4037], // Karlsruhe
[47.9990, 7.8421], // Freiburg
[50.7753, 6.0839] // Aachen
];
const [lat, lon] = demoLocations[index % demoLocations.length];
// Koordinaten zur Warnung hinzufügen für später
warning.demoCoords = { lat, lon };
try {
const lonLat = new OpenLayers.LonLat(lon, lat).transform(
new OpenLayers.Projection("EPSG:4326"),
W.map.getOLMap().getProjectionObject()
);
// Icon basierend auf Warnungstyp und Quelle
const iconUrl = getWarningIcon(warning);
const size = new OpenLayers.Size(32, 32);
const offset = new OpenLayers.Pixel(-16, -16);
const icon = new OpenLayers.Icon(iconUrl, size, offset);
const marker = new OpenLayers.Marker(lonLat, icon);
// Click Event für Marker
marker.events.register('click', marker, function() {
showWarningDetails(warning);
});
ninaLayer.addMarker(marker);
ninaMarkers.push({marker: marker, layer: ninaLayer});
} catch (e) {
console.error(`Fehler beim Erstellen von Marker ${index}:`, e);
}
});
console.log(`${ninaMarkers.length} Marker erfolgreich hinzugefügt`);
}
function getWarningIcon(warning) {
let color = '#ff6b35'; // Default orange
// Farbe basierend auf Schweregrad
if (warning.severity) {
switch(warning.severity.toLowerCase()) {
case 'extreme': color = '#8B0000'; break; // Dunkelrot
case 'severe': color = '#FF0000'; break; // Rot
case 'moderate': color = '#FF8C00'; break; // Orange
case 'minor': color = '#FFD700'; break; // Gelb
default: color = '#ff6b35'; break;
}
}
// Symbol basierend auf Quelle (nur ASCII-Zeichen)
let symbol = '!';
if (warning.source) {
switch(warning.source) {
case 'dwd': symbol = 'W'; break; // Weather
case 'mowas': symbol = '!'; break; // Alert
case 'katwarn': symbol = 'K'; break; // KatWarn
case 'biwapp': symbol = 'B'; break; // BiWapp
case 'lhp': symbol = 'H'; break; // Hochwasser
default: symbol = '!'; break;
}
}
// SVG ohne problematische Unicode-Zeichen
const svgContent = `<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<circle cx="16" cy="16" r="15" fill="${color}" stroke="#ffffff" stroke-width="2"/>
<text x="16" y="20" text-anchor="middle" fill="white" font-size="16" font-weight="bold" font-family="Arial">${symbol}</text>
</svg>`;
try {
return `data:image/svg+xml;base64,${btoa(svgContent)}`;
} catch (e) {
console.error('Fehler beim Erstellen des Icons:', e);
// Fallback: einfaches farbiges Quadrat
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(`<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg"><rect width="32" height="32" fill="${color}"/><text x="16" y="20" text-anchor="middle" fill="white" font-size="16">${symbol}</text></svg>`)}`;
}
}
function updateStats(message) {
const statsEl = document.getElementById('nina-stats');
if (statsEl) {
if (typeof message === 'string') {
statsEl.innerHTML = message;
} else {
const total = ninaWarnings.length;
const sources = {};
ninaWarnings.forEach(w => {
sources[w.source] = (sources[w.source] || 0) + 1;
});
const sourceStats = Object.entries(sources)
.map(([source, count]) => `${source.toUpperCase()}: ${count}`)
.join(' | ');
statsEl.innerHTML = `📊 Gesamt: <strong>${total}</strong> Warnungen | ${sourceStats} | Letztes Update: ${new Date().toLocaleTimeString('de-DE')}`;
}
}
}
function updateTabContent(warnings) {
updateStats();
const content = document.getElementById('nina-warnings-content');
if (!content) return;
if (!warnings || warnings.length === 0) {
content.innerHTML = `
<div class="nina-empty">
<div style="font-size: 2em; margin-bottom: 10px;">✅</div>
<div style="font-weight: bold; margin-bottom: 5px;">Keine aktuellen Warnungen</div>
<div style="font-size: 0.9em;">Alle Systeme normal</div>
</div>
`;
return;
}
const warningsHtml = warnings.map((warning, index) => {
// Titel aus verschiedenen Quellen extrahieren
const title = warning.i18nTitle?.de || warning.headline || warning.title || 'Warnung';
// Zeitinfo formatieren
const startDate = warning.startDate ? new Date(warning.startDate).toLocaleString('de-DE') : '';
const expiresDate = warning.expiresDate ? new Date(warning.expiresDate).toLocaleString('de-DE') : '';
// Schweregrad und Quelle
const severity = warning.severity || 'Unbekannt';
const source = warning.source || 'Unbekannt';
const urgency = warning.urgency || '';
// Status
const type = warning.type || 'Alert';
const isCancel = type === 'Cancel';
return `
<div class="nina-warning" data-warning-index="${index}" style="${isCancel ? 'opacity: 0.6; border-left-color: #95a5a6;' : ''}">
<div class="nina-warning-title">
${isCancel ? '✅ Entwarnung: ' : ''}${title}
<button class="nina-goto-btn" data-goto-index="${index}">📍 Zur Karte</button>
</div>
<div class="nina-warning-meta">
📡 Quelle: ${source.toUpperCase()}<span class="nina-source-badge">${getSourceSymbol(source)}</span>
${startDate ? `<br>🕐 Seit: ${startDate}` : ''}
${expiresDate ? `<br>⏰ Bis: ${expiresDate}` : ''}
</div>
<div>
<span class="nina-warning-type" style="background: ${getSeverityColor(severity)}">${severity}</span>
${urgency ? `<span class="nina-warning-type" style="background: #e74c3c; margin-left: 5px;">${urgency}</span>` : ''}
${isCancel ? '<span class="nina-warning-type" style="background: #27ae60; margin-left: 5px;">Entwarnung</span>' : ''}
</div>
</div>
`;
}).join('');
content.innerHTML = warningsHtml;
// Event Listener für Warnungen hinzufügen
content.querySelectorAll('.nina-warning').forEach(warningEl => {
const index = parseInt(warningEl.dataset.warningIndex);
warningEl.addEventListener('click', () => showWarningDetails(index));
});
// Event Listener für "Zur Karte" Buttons hinzufügen
content.querySelectorAll('.nina-goto-btn').forEach(btn => {
const index = parseInt(btn.dataset.gotoIndex);
btn.addEventListener('click', (event) => {
event.stopPropagation();
gotoWarning(index);
});
});
}
function getSourceSymbol(source) {
switch(source) {
case 'dwd': return 'W';
case 'mowas': return '!';
case 'katwarn': return 'K';
case 'biwapp': return 'B';
case 'lhp': return 'H';
default: return '?';
}
}
function getSeverityColor(severity) {
switch(severity?.toLowerCase()) {
case 'extreme': return '#8B0000';
case 'severe': return '#FF0000';
case 'moderate': return '#FF8C00';
case 'minor': return '#FFD700';
default: return '#ff6b35';
}
}
function showWarningDetails(index) {
const warning = ninaWarnings[index];
if (!warning) return;
const title = warning.i18nTitle?.de || warning.headline || warning.title || 'Warnung';
const description = warning.description || warning.info?.[0]?.description || 'Keine Details verfügbar';
const source = warning.source ? warning.source.toUpperCase() : 'Unbekannte Quelle';
const severity = warning.severity || 'Unbekannt';
const urgency = warning.urgency || 'Unbekannt';
const type = warning.type || 'Alert';
const startDate = warning.startDate ? new Date(warning.startDate).toLocaleString('de-DE') : 'Unbekannt';
const expiresDate = warning.expiresDate ? new Date(warning.expiresDate).toLocaleString('de-DE') : 'Unbekannt';
const isCancel = type === 'Cancel';
const statusText = isCancel ? '✅ ENTWARNUNG' : '🚨 AKTIVE WARNUNG';
alert(`${statusText}\n\n📢 ${title}\n\n📊 Quelle: ${source}\n🔴 Schweregrad: ${severity}\n⚡ Dringlichkeit: ${urgency}\n\n🕐 Gültig seit: ${startDate}\n⏰ Gültig bis: ${expiresDate}\n\n📄 Details:\n${description}`);
}
function toggleMapMarkers() {
const checkbox = document.getElementById('show-markers-checkbox');
showMarkersOnMap = checkbox ? checkbox.checked : true;
if (showMarkersOnMap) {
console.log('Aktiviere Karten-Marker');
// Marker anzeigen
addMarkersToMap(ninaWarnings);
} else {
console.log('Deaktiviere Karten-Marker');
// Marker verstecken
clearMarkers();
}
}
function gotoWarning(index) {
const warning = ninaWarnings[index];
if (!warning || !warning.demoCoords) return;
const { lat, lon } = warning.demoCoords;
// Zur Position auf der Karte springen
const lonLat = new OpenLayers.LonLat(lon, lat).transform(
new OpenLayers.Projection("EPSG:4326"),
W.map.getOLMap().getProjectionObject()
);
// Karte zur Position bewegen
W.map.getOLMap().setCenter(lonLat);
// Optional: Zoom-Level setzen
if (W.map.getOLMap().getZoom() < 5) {
W.map.getOLMap().zoomTo(5);
}
console.log(`Springe zur Warnung: ${warning.i18nTitle?.de || warning.id} (${lat}, ${lon})`);
// Kurz blinken lassen für visuelles Feedback (nur wenn Marker sichtbar)
if (showMarkersOnMap) {
const marker = ninaMarkers[index];
if (marker && marker.marker) {
const originalIcon = marker.marker.icon;
// Temporär rotes Icon setzen
setTimeout(() => {
if (marker.marker.icon) {
const redIcon = getWarningIcon({...warning, severity: 'extreme'});
marker.marker.setUrl(redIcon);
setTimeout(() => {
marker.marker.setUrl(originalIcon.url);
}, 1000);
}
}, 100);
}
}
}
// Globale Funktionen entfernen - nicht mehr benötigt
// window.showWarningDetails = showWarningDetails;
// Script initialisieren
function initializeScript() {
if (W?.userscripts?.state.isInitialized) {
initNINAScript();
} else {
document.addEventListener("wme-initialized", initNINAScript, {
once: true,
});
}
}
// Script starten
console.log('NINA WME Script gestartet');
initializeScript();
})();