您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Displays German Autobahn traffic information in Waze Map Editor
// ==UserScript== // @name Autobahn Traffic Overlay // @namespace https://greasyfork.org/de/users/863740-horst-wittlich // @version 2025.08.18 // @description Displays German Autobahn traffic information in Waze Map Editor // @author Hiwi234 // @match https://www.waze.com/editor* // @match https://www.waze.com/*/editor* // @match https://beta.waze.com/editor* // @match https://beta.waze.com/*/editor* // @grant GM_xmlhttpRequest // @connect verkehr.autobahn.de // @require https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js // @license MIT // Calculate distance between two points in kilometers function calculateDistance(lat1, lon1, lat2, lon2) { const R = 6371; // Earth's radius in kilometers const dLat = (lat2 - lat1) * Math.PI / 180; const dLon = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon/2) * Math.sin(dLon/2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); return R * c; } // ==/UserScript== (function() { 'use strict'; const SCRIPT_ID = 'vz-deutschland-overlay'; const SCRIPT_NAME = 'VZ Deutschland Traffic'; const API_BASE = 'https://verkehr.autobahn.de/o/autobahn'; // List of all German Autobahns const AUTOBAHNS = [ 'A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8', 'A9', 'A10', 'A11', 'A12', 'A13', 'A14', 'A15', 'A17', 'A19', 'A20', 'A21', 'A23', 'A24', 'A25', 'A26', 'A27', 'A28', 'A29', 'A30', 'A31', 'A33', 'A36', 'A37', 'A38', 'A39', 'A40', 'A42', 'A43', 'A44', 'A45', 'A46', 'A48', 'A49', 'A52', 'A57', 'A59', 'A60', 'A61', 'A62', 'A63', 'A64', 'A65', 'A66', 'A67', 'A70', 'A71', 'A72', 'A73', 'A81', 'A92', 'A93', 'A94', 'A95', 'A96', 'A98', 'A99', 'A100', 'A103', 'A111', 'A113', 'A114', 'A115' ]; let isOverlayVisible = false; let overlayLayer = null; let vectorLayer = null; // For line segments let tabPane = null; let trafficMarkers = []; let trafficFeatures = []; // For line features let updateInterval = null; let currentRequests = []; let lastUpdateTime = null; let currentEventsData = []; // Store current events data for re-sorting let mapMoveTimeout = null; // For debouncing map movement updates let lastMapCenter = null; // Track last map center let lastMapZoom = null; // Track last map zoom let currentTooltip = null; // Track current hover tooltip // Initialize the userscript when WME is ready function initializeScript() { console.log(`${SCRIPT_NAME}: Initializing...`); try { // Register sidebar tab const { tabLabel, tabPane: pane } = W.userscripts.registerSidebarTab(SCRIPT_ID); tabPane = pane; // Set up tab label tabLabel.innerHTML = '<i class="fa fa-road" style="font-size: 16px;"></i>'; tabLabel.title = SCRIPT_NAME; tabLabel.style.cursor = 'pointer'; // Wait for tab pane to be connected to DOM W.userscripts.waitForElementConnected(tabPane).then(() => { setupTabContent(); createOverlayLayer(); // Auto-enable overlay after setup setTimeout(() => { const overlayToggle = document.getElementById('vzOverlayToggle'); if (overlayToggle && overlayToggle.checked) { showOverlay(); } }, 500); }); console.log(`${SCRIPT_NAME}: Successfully initialized`); } catch (error) { console.error(`${SCRIPT_NAME}: Failed to initialize:`, error); } } // Create OpenLayers overlay layer function createOverlayLayer() { if (!W.map) return; // Create layer for point markers overlayLayer = new OpenLayers.Layer.Markers("German Autobahn Traffic", { displayInLayerSwitcher: false, uniqueName: "__autobahnTraffic" }); // Create vector layer for line segments (affected road sections) with WME compatible styling vectorLayer = new OpenLayers.Layer.Vector("German Autobahn Sections", { displayInLayerSwitcher: false, uniqueName: "__autobahnSections", renderers: ["SVG", "VML", "Canvas"], // Ensure compatibility styleMap: new OpenLayers.StyleMap({ "default": new OpenLayers.Style({ strokeColor: "#ff4444", strokeWidth: 4, strokeOpacity: 0.8, strokeDasharray: "10,5", fillOpacity: 0 }) }) }); overlayLayer.setZIndex(2000); vectorLayer.setZIndex(1999); // Behind markers but above map } // Setup the content of the sidebar tab function setupTabContent() { tabPane.innerHTML = ` <div style="padding: 10px;"> <h3 style="margin-top: 0;">German Autobahn Traffic</h3> <div style="margin-bottom: 10px;"> <label style="display: block; margin-bottom: 5px;"> <input type="checkbox" id="vzOverlayToggle" checked style="margin-right: 5px;"> Show Traffic Overlay </label> <label style="display: block; margin-bottom: 5px; margin-left: 20px;"> <input type="checkbox" id="showAffectedSections" checked style="margin-right: 5px;"> Show affected road sections as lines </label> </div> <div style="margin-bottom: 10px;"> <h4>Data Types:</h4> <label style="display: block; margin-bottom: 3px;"> <input type="checkbox" class="vz-source" data-source="roadworks" checked style="margin-right: 5px;"> 🚧 Construction Sites (Baustellen) </label> <label style="display: block; margin-bottom: 3px;"> <input type="checkbox" class="vz-source" data-source="warning" checked style="margin-right: 5px;"> ⚠️ Traffic Warnings (Verkehrsmeldungen) </label> <label style="display: block; margin-bottom: 3px;"> <input type="checkbox" class="vz-source" data-source="closure" checked style="margin-right: 5px;"> 🚫 Road Closures (Sperrungen) </label> </div> <div style="margin-bottom: 10px;"> <label style="display: block; margin-bottom: 5px;"> <input type="checkbox" id="autoUpdateEnabled" checked style="margin-right: 5px;"> Enable automatic updates </label> <div id="updateIntervalContainer" style="margin-left: 20px;"> <label style="display: block; margin-bottom: 5px;"> Update Interval: <span id="intervalValue">120</span>s </label> <input type="range" id="intervalSlider" min="30" max="600" step="30" value="120" style="width: 100%;"> <div style="font-size: 11px; color: #666; margin-top: 3px;"> Range: 30s - 10min (Map movement updates: Smart filtering) </div> </div> </div> <div style="margin-bottom: 10px;"> <button id="refreshOverlay" style="padding: 5px 10px; margin-right: 5px;"> 🔄 Refresh Data </button> <button id="clearCache" style="padding: 5px 10px;"> 🗑️ Clear Cache </button> </div> <div id="statusDisplay" style="margin-top: 10px; padding: 8px; border: 1px solid #ccc; border-radius: 3px; background: #f9f9f9; font-size: 12px;"> Status: Ready to load traffic data </div> <div id="statisticsDisplay" style="margin-top: 10px; padding: 8px; border: 1px solid #ddd; border-radius: 3px; background: #f0f8ff; font-size: 11px;"> <strong>Statistics:</strong><br> Last Update: Never<br> Total Events: 0<br> Active Requests: 0 </div> <!-- Traffic Events List --> <div id="eventsListContainer" style="margin-top: 15px; border-top: 2px solid #ddd; padding-top: 10px;"> <h4 style="margin: 0 0 10px 0; display: flex; justify-content: space-between; align-items: center;"> Traffic Events <span id="eventsCount" style="font-size: 12px; background: #007cba; color: white; padding: 2px 6px; border-radius: 3px;"> 0 </span> </h4> <!-- Sorting Controls --> <div style="margin-bottom: 10px; padding: 5px; background: #f0f8ff; border-radius: 3px; border: 1px solid #ddd;"> <label style="font-size: 11px; margin-right: 10px;"> <strong>Sortierung:</strong> </label> <label style="font-size: 11px; margin-right: 15px;"> <input type="radio" name="sortBy" value="distance" checked style="margin-right: 3px;"> Entfernung </label> <label style="font-size: 11px; margin-right: 15px;"> <input type="radio" name="sortBy" value="date" style="margin-right: 3px;"> Datum (neu→alt) </label> <label style="font-size: 11px; margin-right: 15px;"> <input type="radio" name="sortBy" value="dateOld" style="margin-right: 3px;"> Datum (alt→neu) </label> <label style="font-size: 11px;"> <input type="radio" name="sortBy" value="future" style="margin-right: 3px;"> Zukünftige Ereignisse </label> </div> <div id="eventsList" style="max-height: 300px; overflow-y: auto; border: 1px solid #ccc; border-radius: 3px; background: #fafafa;"> <div style="padding: 10px; text-align: center; color: #666; font-size: 12px;"> No events loaded yet </div> </div> </div> <div style="margin-top: 10px; font-size: 11px; color: #666;"> <p><strong>Real Data Source:</strong> verkehr.autobahn.de</p> <p>Shows live traffic information from German Federal Highways. Data updates automatically based on your current map view.</p> <p><strong>Tooltip:</strong> Hover over markers to see details. Click on markers to show persistent popup.</p> </div> </div> `; setupEventListeners(); } // Setup event listeners for the controls function setupEventListeners() { const overlayToggle = document.getElementById('vzOverlayToggle'); const intervalSlider = document.getElementById('intervalSlider'); const intervalValue = document.getElementById('intervalValue'); const refreshButton = document.getElementById('refreshOverlay'); const clearCacheButton = document.getElementById('clearCache'); const sourceCheckboxes = document.querySelectorAll('.vz-source'); const autoUpdateCheckbox = document.getElementById('autoUpdateEnabled'); const intervalContainer = document.getElementById('updateIntervalContainer'); overlayToggle.addEventListener('change', function() { if (this.checked) { showOverlay(); } else { hideOverlay(); } }); intervalSlider.addEventListener('input', function() { intervalValue.textContent = this.value; if (updateInterval && autoUpdateCheckbox.checked) { clearInterval(updateInterval); startAutoUpdate(parseInt(this.value) * 1000); } }); // Auto-update toggle autoUpdateCheckbox.addEventListener('change', function() { if (this.checked) { // Enable auto-update intervalContainer.style.opacity = '1'; intervalContainer.style.pointerEvents = 'auto'; if (isOverlayVisible) { const interval = parseInt(intervalSlider.value) * 1000; startAutoUpdate(interval); } updateStatus('Automatic updates enabled'); } else { // Disable auto-update intervalContainer.style.opacity = '0.5'; intervalContainer.style.pointerEvents = 'none'; if (updateInterval) { clearInterval(updateInterval); updateInterval = null; } updateStatus('Automatic updates disabled - manual refresh only'); } }); refreshButton.addEventListener('click', function() { if (isOverlayVisible) { updateTrafficData(); } }); clearCacheButton.addEventListener('click', function() { clearMarkers(); clearFeatures(); updateEventsList([]); updateStatus('Cache cleared'); }); sourceCheckboxes.forEach(checkbox => { checkbox.addEventListener('change', function() { if (isOverlayVisible) { updateTrafficData(); } }); }); // Add event listeners for sorting radio buttons and section display document.addEventListener('change', function(e) { if (e.target.name === 'sortBy') { // Re-sort and update the events list const currentEvents = getCurrentEventsData(); if (currentEvents && currentEvents.length > 0) { updateEventsList(currentEvents); } } else if (e.target.id === 'showAffectedSections') { // Toggle road sections display if (isOverlayVisible) { const currentEvents = getCurrentEventsData(); if (currentEvents && currentEvents.length > 0) { clearFeatures(); // Clear existing features const bounds = W.map.getExtent(); const displayData = currentEvents.filter(item => isItemInBounds(item, bounds)); displayData.forEach(item => { addTrafficMarker(item); if (e.target.checked) { addRoadSection(item); } }); } } } }); // Listen for map movement events with debouncing if (W.map && W.map.events) { W.map.events.register('moveend', null, function() { if (isOverlayVisible) { handleMapMovement(); } }); W.map.events.register('zoomend', null, function() { if (isOverlayVisible) { handleMapMovement(); } }); } } // Show the traffic overlay function showOverlay() { if (!overlayLayer || !vectorLayer || !W.map) return; W.map.addLayer(vectorLayer); // Add vector layer first (behind markers) W.map.addLayer(overlayLayer); // Add vector layer interaction controls setupVectorLayerEvents(); isOverlayVisible = true; updateTrafficData(); // Only start auto-update if enabled const autoUpdateEnabled = document.getElementById('autoUpdateEnabled')?.checked; if (autoUpdateEnabled) { const interval = parseInt(document.getElementById('intervalSlider').value) * 1000; startAutoUpdate(interval); } updateStatus('Overlay enabled - Loading traffic data...'); } // Setup vector layer events for road sections function setupVectorLayerEvents() { if (!vectorLayer) return; console.log('Setting up vector layer events...'); // Use WME-compatible event handling approach const selectControl = new OpenLayers.Control.SelectFeature(vectorLayer, { onSelect: function(feature) { console.log('Feature selected:', feature); if (feature.eventData) { const bounds = feature.geometry.getBounds(); const centerPoint = bounds.getCenterLonLat(); // Create a mouse event for tooltip positioning const mockEvent = { clientX: window.innerWidth / 2, clientY: window.innerHeight / 2 }; const tooltipContent = createTooltipContent(feature.eventData); showTooltip(mockEvent, tooltipContent); } }, onUnselect: function(feature) { // Optional: handle unselect } }); // Add and activate the control W.map.addControl(selectControl); selectControl.activate(); // Store reference for cleanup vectorLayer.selectControl = selectControl; console.log('Vector layer events set up successfully'); } // Hide the traffic overlay function hideOverlay() { if (overlayLayer && W.map) { W.map.removeLayer(overlayLayer); } if (vectorLayer && W.map) { // Remove select control if exists if (vectorLayer.selectControl) { W.map.removeControl(vectorLayer.selectControl); vectorLayer.selectControl = null; } W.map.removeLayer(vectorLayer); } isOverlayVisible = false; if (updateInterval) { clearInterval(updateInterval); updateInterval = null; } // Cancel pending requests and map movement timeouts currentRequests.forEach(request => { if (request.abort) request.abort(); }); currentRequests = []; if (mapMoveTimeout) { clearTimeout(mapMoveTimeout); mapMoveTimeout = null; } // Hide any tooltip hideTooltip(); updateStatus('Overlay disabled'); updateStatistics(); } // Start automatic updates (only if enabled) function startAutoUpdate(interval) { if (updateInterval) { clearInterval(updateInterval); } const autoUpdateEnabled = document.getElementById('autoUpdateEnabled')?.checked; if (!autoUpdateEnabled) { updateStatus('Automatic updates disabled'); return; } updateInterval = setInterval(() => { if (isOverlayVisible && autoUpdateEnabled) { updateTrafficData(); } }, interval); updateStatus(`Automatic updates enabled - every ${interval/1000} seconds`); } // Main function to update traffic data function updateTrafficData() { if (!overlayLayer || !W.map) return; const zoom = W.map.getZoom(); if (zoom < 8) { updateStatus('Zoom in to see traffic data (minimum zoom level 8)'); return; } updateStatus('Fetching traffic data from autobahn.de...'); // Cancel any pending requests currentRequests.forEach(request => { if (request.abort) request.abort(); }); currentRequests = []; // Clear existing markers and features clearMarkers(); clearFeatures(); const selectedSources = getSelectedSources(); if (selectedSources.length === 0) { updateStatus('No data types selected'); return; } // Get relevant autobahns for current view const relevantAutobahns = getRelevantAutobahns(); updateStatus(`Loading data for ${relevantAutobahns.length} autobahns...`); let completedRequests = 0; let totalRequests = relevantAutobahns.length * selectedSources.length; let allData = []; // Fetch data for each autobahn and data type relevantAutobahns.forEach(autobahn => { selectedSources.forEach(dataType => { fetchAutobahnData(autobahn, dataType) .then(data => { if (data && data.length > 0) { allData = allData.concat(data.map(item => ({...item, autobahn, dataType}))); } completedRequests++; if (completedRequests === totalRequests) { // All requests completed processTrafficData(allData); lastUpdateTime = new Date(); updateStatus(`Data loaded successfully - ${allData.length} events found`); updateStatistics(); } else { updateStatus(`Loading... ${completedRequests}/${totalRequests} requests completed`); } }) .catch(error => { console.error(`Error fetching ${dataType} for ${autobahn}:`, error); completedRequests++; if (completedRequests === totalRequests) { processTrafficData(allData); lastUpdateTime = new Date(); updateStatus(`Data loaded with errors - ${allData.length} events found`); updateStatistics(); } }); }); }); updateStatistics(); } // Fetch data from autobahn API function fetchAutobahnData(autobahn, dataType) { return new Promise((resolve, reject) => { const url = `${API_BASE}/${autobahn}/services/${dataType}`; const request = GM_xmlhttpRequest({ method: 'GET', url: url, headers: { 'Accept': 'application/json', 'User-Agent': 'WME VZ Deutschland Overlay' }, timeout: 15000, onload: function(response) { try { if (response.status === 200) { const data = JSON.parse(response.responseText); resolve(data[dataType] || []); } else { console.warn(`HTTP ${response.status} for ${autobahn}/${dataType}`); resolve([]); } } catch (error) { console.error(`Parse error for ${autobahn}/${dataType}:`, error); resolve([]); } // Remove from active requests const index = currentRequests.indexOf(request); if (index > -1) currentRequests.splice(index, 1); }, onerror: function(error) { console.error(`Request error for ${autobahn}/${dataType}:`, error); reject(error); // Remove from active requests const index = currentRequests.indexOf(request); if (index > -1) currentRequests.splice(index, 1); }, ontimeout: function() { console.warn(`Timeout for ${autobahn}/${dataType}`); resolve([]); // Remove from active requests const index = currentRequests.indexOf(request); if (index > -1) currentRequests.splice(index, 1); } }); currentRequests.push(request); }); } // Get autobahns relevant for current map view function getRelevantAutobahns() { // Get current map bounds in WGS84 const bounds = W.map.getExtent(); const center = bounds.getCenterLonLat().transform( new OpenLayers.Projection("EPSG:3857"), new OpenLayers.Projection("EPSG:4326") ); console.log('Current center coordinates:', center.lat, center.lon); // More comprehensive autobahn selection based on geographic regions let relevantAutobahns = []; // Check if we're in Germany at all (rough bounds) if (center.lat < 47.3 || center.lat > 55.1 || center.lon < 5.9 || center.lon > 15.0) { // Outside Germany - use major autobahns relevantAutobahns = ['A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8', 'A9']; } else { // Within Germany - regional selection if (center.lat > 53.5) { // Schleswig-Holstein, Hamburg relevantAutobahns = ['A1', 'A7', 'A20', 'A21', 'A23', 'A24', 'A25']; } else if (center.lat > 52.5) { // Lower Saxony, Bremen, Mecklenburg-Vorpommern relevantAutobahns = ['A1', 'A2', 'A7', 'A14', 'A19', 'A20', 'A24', 'A27', 'A28', 'A29', 'A30', 'A31']; } else if (center.lat > 51.5) { // North Rhine-Westphalia, Saxony-Anhalt, Brandenburg relevantAutobahns = ['A1', 'A2', 'A3', 'A4', 'A30', 'A31', 'A33', 'A37', 'A38', 'A39', 'A40', 'A42', 'A43', 'A44', 'A45', 'A46']; } else if (center.lat > 50.0) { // Hesse, Thuringia, Saxony, parts of Rhineland-Palatinate relevantAutobahns = ['A3', 'A4', 'A5', 'A6', 'A7', 'A9', 'A38', 'A40', 'A44', 'A45', 'A48', 'A60', 'A61', 'A66', 'A67', 'A71', 'A72', 'A73']; } else if (center.lat > 48.5) { // Baden-Württemberg, Bavaria (north) relevantAutobahns = ['A3', 'A5', 'A6', 'A7', 'A8', 'A9', 'A60', 'A61', 'A63', 'A65', 'A67', 'A70', 'A73', 'A81']; } else { // Bavaria (south), parts of Baden-Württemberg relevantAutobahns = ['A3', 'A5', 'A6', 'A8', 'A9', 'A70', 'A92', 'A93', 'A94', 'A95', 'A96', 'A99']; } } console.log('Selected autobahns for region:', relevantAutobahns); return relevantAutobahns; } // Process and display traffic data function processTrafficData(allData) { if (!allData || allData.length === 0) { updateStatus('No traffic events found for current area'); updateEventsList([]); return; } console.log('Processing', allData.length, 'total events'); // Log some sample data to understand structure if (allData.length > 0) { console.log('Sample event:', allData[0]); } // Get current map bounds const bounds = W.map.getExtent(); console.log('Current map bounds (Web Mercator):', bounds); // Transform bounds to WGS84 for logging const boundsWGS84 = bounds.clone().transform(new OpenLayers.Projection("EPSG:3857"), new OpenLayers.Projection("EPSG:4326")); console.log('Current map bounds (WGS84):', boundsWGS84); // Calculate center for distance calculations const center = bounds.getCenterLonLat().transform(new OpenLayers.Projection("EPSG:3857"), new OpenLayers.Projection("EPSG:4326")); // Add distance and process timestamps for all events const allDataWithDistance = allData .filter(item => item.coordinate && item.coordinate.lat && item.coordinate.long) .map(item => { const distance = calculateDistance(center.lat, center.lon, item.coordinate.lat, item.coordinate.long); // Process timestamp for sorting let timestamp = null; let formattedDate = 'Unbekannt'; if (item.startTimestamp) { try { timestamp = new Date(item.startTimestamp); formattedDate = timestamp.toLocaleDateString('de-DE') + ' ' + timestamp.toLocaleTimeString('de-DE', {hour: '2-digit', minute: '2-digit'}); } catch (e) { console.warn('Invalid timestamp for event:', item.identifier, item.startTimestamp); } } return { ...item, distance, timestamp, formattedDate }; }); // Store current data for re-sorting currentEventsData = allDataWithDistance; // Filter data based on current map bounds const filteredData = allDataWithDistance.filter(item => { return isItemInBounds(item, bounds); }); console.log('Filtered to', filteredData.length, 'events in current view'); // If no events in current bounds, show nearby events let displayData = filteredData; if (filteredData.length === 0 && allDataWithDistance.length > 0) { displayData = allDataWithDistance.slice(0, 10); // Show 10 closest events console.log('Showing 10 nearest events instead'); updateStatus(`No events in current view. Showing ${displayData.length} nearest events (closest: ${displayData[0]?.distance?.toFixed(1)}km away)`); } else { updateStatus(`Displaying ${filteredData.length} of ${allData.length} traffic events in current view`); } // Add markers and road sections for display data const showSections = document.getElementById('showAffectedSections')?.checked || false; displayData.forEach(item => { addTrafficMarker(item); if (showSections) { addRoadSection(item); } }); // Update events list with all data (sorted according to current selection) updateEventsList(allDataWithDistance); } // Get current events data for re-sorting function getCurrentEventsData() { return currentEventsData; } // Sort events based on selected criteria function sortEvents(events, sortBy) { const sorted = [...events]; switch (sortBy) { case 'distance': return sorted.sort((a, b) => a.distance - b.distance); case 'date': // Newest first (newest dates have higher timestamp values) return sorted.sort((a, b) => { if (!a.timestamp && !b.timestamp) return 0; if (!a.timestamp) return 1; // Events without date go to the end if (!b.timestamp) return -1; return b.timestamp - a.timestamp; // Newest first }); case 'dateOld': // Oldest first (oldest dates have lower timestamp values) return sorted.sort((a, b) => { if (!a.timestamp && !b.timestamp) return 0; if (!a.timestamp) return 1; // Events without date go to the end if (!b.timestamp) return -1; return a.timestamp - b.timestamp; // Oldest first }); case 'future': // Future events first, then by distance return sorted.sort((a, b) => { // First sort by future status if (a.future && !b.future) return -1; if (!a.future && b.future) return 1; // If both have same future status, sort by distance return a.distance - b.distance; }); default: return sorted; } } // Update the events list in the sidebar function updateEventsList(events) { const eventsList = document.getElementById('eventsList'); const eventsCount = document.getElementById('eventsCount'); if (!eventsList) return; if (!events || events.length === 0) { eventsList.innerHTML = ` <div style="padding: 10px; text-align: center; color: #666; font-size: 12px;"> No events loaded yet </div> `; if (eventsCount) eventsCount.textContent = '0'; return; } // Get selected sorting method const sortBy = document.querySelector('input[name="sortBy"]:checked')?.value || 'distance'; const sortedEvents = sortEvents(events, sortBy); if (eventsCount) eventsCount.textContent = events.length; const typeNames = { roadworks: '🚧 Baustelle', warning: '⚠️ Verkehrsmeldung', closure: '🚫 Sperrung' }; const typeColors = { roadworks: '#ff9800', warning: '#f44336', closure: '#9c27b0' }; let listHTML = ''; sortedEvents.slice(0, 1000).forEach((event, index) => { // Increased limit to 1000 events const typeName = typeNames[event.dataType] || event.dataType; const typeColor = typeColors[event.dataType] || '#2196f3'; const title = event.title || 'Keine Beschreibung'; const autobahn = event.autobahn || 'Unbekannt'; const distance = event.distance ? `${event.distance.toFixed(1)} km` : 'Unbekannt'; const dateInfo = event.formattedDate || 'Unbekannt'; // Add visual indicator for future events const futureIndicator = event.future ? ' style="background: #d1ecf1; border-left-color: #17a2b8 !important;"' : ''; const futureIcon = event.future ? ' 🔮' : ''; // Create secondary info based on sort order let secondaryInfo = ''; if (sortBy === 'distance') { secondaryInfo = `<span>Entfernung: ${distance}</span><span>Datum: ${dateInfo}</span>`; } else if (sortBy === 'future') { const futureStatus = event.future ? '🔮 Zukünftig' : '📅 Aktuell'; secondaryInfo = `<span>${futureStatus}</span><span>Entfernung: ${distance}</span>`; } else { secondaryInfo = `<span>Datum: ${dateInfo}</span><span>Entfernung: ${distance}</span>`; } listHTML += ` <div class="event-item" data-event-id="${event.identifier}" style=" padding: 8px; border-bottom: 1px solid #ddd; cursor: pointer; background: white; border-left: 4px solid ${typeColor}; ${futureIndicator.slice(7, -1)} // Remove 'style="' and '"' " onmouseover="this.style.background='#f0f8ff'" onmouseout="this.style.background='${event.future ? '#d1ecf1' : 'white'}'"> <div style="font-weight: bold; font-size: 12px; color: #333; margin-bottom: 3px;"> ${typeName}${futureIcon} - ${autobahn} </div> <div style="font-size: 11px; color: #666; margin-bottom: 3px;"> ${title} </div> <div style="font-size: 10px; color: #999; display: flex; justify-content: space-between;"> ${secondaryInfo} </div> <div style="font-size: 9px; color: #007cba; text-align: right; margin-top: 2px;"> Zur Position springen → </div> </div> `; }); if (events.length > 1000) { listHTML += ` <div style="padding: 8px; text-align: center; color: #666; font-size: 11px; background: #f5f5f5;"> ... und ${events.length - 1000} weitere Events (nur die ersten 1000 werden angezeigt) </div> `; } eventsList.innerHTML = listHTML; // Add click event listeners to event items eventsList.querySelectorAll('.event-item').forEach(item => { item.addEventListener('click', function() { const eventId = this.dataset.eventId; jumpToEvent(eventId, sortedEvents); }); }); } // Jump to specific event on map (simplified - no popup) function jumpToEvent(eventId, events) { const event = events.find(e => e.identifier === eventId); if (!event || !event.coordinate) { console.error('Event not found or has no coordinates:', eventId); return; } try { // Transform coordinates to map projection const lonLat = new OpenLayers.LonLat(event.coordinate.long, event.coordinate.lat) .transform(new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:3857")); // Center map on event W.map.setCenter(lonLat, Math.max(W.map.getZoom(), 12)); updateStatus(`Jumped to ${event.dataType}: ${event.title || event.identifier}`); } catch (error) { console.error('Error jumping to event:', error); updateStatus('Error jumping to event'); } } // Handle map movement with intelligent updates function handleMapMovement() { // Clear any pending update if (mapMoveTimeout) { clearTimeout(mapMoveTimeout); } try { // Get current map state const currentCenter = W.map.getCenter(); const currentZoom = W.map.getZoom(); // Check if we need to update based on movement distance and zoom const shouldUpdate = checkIfUpdateNeeded(currentCenter, currentZoom); if (shouldUpdate) { // Only auto-update if enabled const autoUpdateEnabled = document.getElementById('autoUpdateEnabled')?.checked; if (autoUpdateEnabled) { // Debounce the update - wait 3 seconds after movement stops mapMoveTimeout = setTimeout(() => { updateStatus('Map moved significantly - updating traffic data...'); lastMapCenter = new OpenLayers.LonLat(currentCenter.lon, currentCenter.lat); lastMapZoom = currentZoom; updateTrafficData(); }, 3000); } else { // Just update tracking without data fetch lastMapCenter = new OpenLayers.LonLat(currentCenter.lon, currentCenter.lat); lastMapZoom = currentZoom; mapMoveTimeout = setTimeout(() => { updateStatus('Map moved - automatic updates disabled (use refresh button)'); refilterExistingData(); }, 1000); } } else { // Just re-filter existing data for new viewport mapMoveTimeout = setTimeout(() => { updateStatus('Re-filtering existing traffic data for new view...'); refilterExistingData(); }, 1000); } } catch (error) { console.error('Error in handleMapMovement:', error); updateStatus('Error handling map movement'); } } // Check if update is needed based on movement distance function checkIfUpdateNeeded(currentCenter, currentZoom) { try { // Always update on first load if (!lastMapCenter || !lastMapZoom) { return true; } // Always update if zoom changed significantly if (Math.abs(currentZoom - lastMapZoom) >= 2) { return true; } // Calculate movement distance in map units const distance = Math.sqrt( Math.pow(currentCenter.lon - lastMapCenter.lon, 2) + Math.pow(currentCenter.lat - lastMapCenter.lat, 2) ); // Update threshold based on zoom level (more movement needed at higher zooms) const updateThreshold = 100000 / Math.pow(2, currentZoom - 10); // Adjust threshold by zoom return distance > updateThreshold; } catch (error) { console.error('Error in checkIfUpdateNeeded:', error); return true; // Safe fallback: allow update } } // Re-filter existing data for new viewport without fetching new data function refilterExistingData() { if (!currentEventsData || currentEventsData.length === 0) { return; } try { // Clear current markers and features clearMarkers(); clearFeatures(); // Get current map bounds const bounds = W.map.getExtent(); // Filter existing data for current bounds const filteredData = currentEventsData.filter(item => { return isItemInBounds(item, bounds); }); // If no events in current bounds, show nearby events let displayData = filteredData; if (filteredData.length === 0 && currentEventsData.length > 0) { // Recalculate distances for current center const center = bounds.getCenterLonLat().transform( new OpenLayers.Projection("EPSG:3857"), new OpenLayers.Projection("EPSG:4326") ); const eventsWithNewDistances = currentEventsData.map(item => ({ ...item, distance: calculateDistance(center.lat, center.lon, item.coordinate.lat, item.coordinate.long) })).sort((a, b) => a.distance - b.distance); displayData = eventsWithNewDistances.slice(0, 10); updateStatus(`No events in current view. Showing ${displayData.length} nearest events (using cached data)`); } else { updateStatus(`Re-filtered to ${filteredData.length} events in current view (using cached data)`); } // Add markers and road sections for display data const showSections = document.getElementById('showAffectedSections')?.checked || false; displayData.forEach(item => { addTrafficMarker(item); if (showSections) { addRoadSection(item); } }); updateStatistics(); } catch (error) { console.error('Error in refilterExistingData:', error); updateStatus('Error re-filtering data'); } } // Check if item is within current map bounds function isItemInBounds(item, bounds) { if (!item.coordinate || (!item.coordinate.lat && item.coordinate.lat !== 0) || (!item.coordinate.long && item.coordinate.long !== 0)) { console.log('Invalid coordinates for item:', item.identifier, item.coordinate); return false; } try { // Transform bounds to WGS84 for comparison const boundsWGS84 = bounds.clone().transform(new OpenLayers.Projection("EPSG:3857"), new OpenLayers.Projection("EPSG:4326")); // Check if item coordinates are within bounds const lat = parseFloat(item.coordinate.lat); const lng = parseFloat(item.coordinate.long); if (isNaN(lat) || isNaN(lng)) { console.log('Invalid coordinate values:', lat, lng, 'for item:', item.identifier); return false; } const isInBounds = lat >= boundsWGS84.bottom && lat <= boundsWGS84.top && lng >= boundsWGS84.left && lng <= boundsWGS84.right; if (!isInBounds) { console.log('Item outside bounds:', item.identifier, 'coords:', lat, lng, 'bounds:', boundsWGS84); } return isInBounds; } catch (error) { console.error('Error checking bounds for item:', item.identifier, error); return false; } } // Add a traffic marker to the map with FIXED event handling function addTrafficMarker(item) { if (!item.coordinate || (!item.coordinate.lat && item.coordinate.lat !== 0) || (!item.coordinate.long && item.coordinate.long !== 0)) { console.log('Skipping item with invalid coordinates:', item.identifier); return; } try { const lat = parseFloat(item.coordinate.lat); const lng = parseFloat(item.coordinate.long); if (isNaN(lat) || isNaN(lng)) { console.log('Skipping item with NaN coordinates:', item.identifier, lat, lng); return; } const lonLat = new OpenLayers.LonLat(lng, lat) .transform(new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:3857")); // Create marker icon based on data type const size = new OpenLayers.Size(24, 24); const offset = new OpenLayers.Pixel(-(size.w/2), -size.h); const iconUrl = createMarkerIcon(item.dataType); const icon = new OpenLayers.Icon(iconUrl, size, offset); const marker = new OpenLayers.Marker(lonLat, icon); // FIXED: Simplified event handling - hover shows tooltip, click shows persistent popup marker.events.register('mouseover', marker, function(e) { const tooltipContent = createTooltipContent(item); const mouseEvent = e.originalEvent || e; showHoverTooltip(mouseEvent, tooltipContent); }); marker.events.register('mouseout', marker, function(e) { hideHoverTooltip(); }); marker.events.register('click', marker, function(e) { const tooltipContent = createTooltipContent(item); const mouseEvent = e.originalEvent || e; showPersistentPopup(mouseEvent, tooltipContent); // Don't stop propagation - let click pass through to WME }); // Store marker data marker.trafficData = item; overlayLayer.addMarker(marker); trafficMarkers.push({marker, data: item}); console.log('Added marker for:', item.identifier, 'at', lat, lng); } catch (error) { console.error('Error adding marker for item:', item.identifier, error); } } // Parse event description for structured information with enhanced closure details function parseEventDescription(description, eventType) { const info = { begin: '', end: '', measure: '', restrictions: '', situation: '', location: '', additional: '', closureType: '', affectedVehicles: '', landmark: '', closedSection: '', alternativeRoute: '', closureReason: '', detour: '', sectionLength: 0 }; if (!Array.isArray(description)) { return info; } let additionalLines = []; let inRestrictions = false; description.forEach((line, index) => { const trimmedLine = line.trim(); if (!trimmedLine) return; // Skip empty lines if (trimmedLine.startsWith('Beginn:')) { info.begin = trimmedLine; } else if (trimmedLine.startsWith('Ende:')) { info.end = trimmedLine; } else if (trimmedLine.startsWith('Art der Maßnahme:')) { info.measure = trimmedLine.replace('Art der Maßnahme:', '').trim(); } else if (trimmedLine.startsWith('Einschränkungen:')) { info.restrictions = trimmedLine.replace('Einschränkungen:', '').trim().replace(/\\n/g, '<br>'); inRestrictions = true; } else if (inRestrictions && !trimmedLine.includes(':') && !trimmedLine.includes('gesperrt')) { // Continue restrictions on next lines info.restrictions += '<br>' + trimmedLine.replace(/\\n/g, '<br>'); } else if (trimmedLine.includes('zwischen ') || trimmedLine.includes('bei ') || trimmedLine.includes('von ') || trimmedLine.includes('bis ')) { // Location information - enhanced for closures info.location = trimmedLine; // Extract specific closure section details if (eventType === 'closure' || eventType === 'roadworks') { // Try to extract closed section from location const sectionMatch = trimmedLine.match(/(zwischen|von)\s+([^-]+)\s*-?\s*(bis|und)?\s*([^,]*)/i); if (sectionMatch) { const start = sectionMatch[2]?.trim(); const end = sectionMatch[4]?.trim(); if (start && end) { info.closedSection = `${start} → ${end}`; } else if (start) { info.closedSection = start; } } } inRestrictions = false; } else if ((eventType === 'closure' || eventType === 'roadworks') && ( trimmedLine.includes('gesperrt') || trimmedLine.includes('Sperrung') || trimmedLine.includes('Vollsperrung') || trimmedLine.includes('blockiert') || trimmedLine.includes('Baustelle') )) { // Enhanced closure type analysis info.closureType = trimmedLine; // Extract closure reason if (trimmedLine.includes('Unfall')) { info.closureReason = 'Verkehrsunfall'; } else if (trimmedLine.includes('Baustelle')) { info.closureReason = 'Bauarbeiten'; } else if (trimmedLine.includes('Bergung')) { info.closureReason = 'Bergungsarbeiten'; } else if (trimmedLine.includes('Defekt') || trimmedLine.includes('Panne')) { info.closureReason = 'Fahrzeugpanne'; } else if (trimmedLine.includes('Wetter') || trimmedLine.includes('Glätte') || trimmedLine.includes('Schnee')) { info.closureReason = 'Witterungsbedingungen'; } else if (trimmedLine.includes('Kontrolle') || trimmedLine.includes('Polizei')) { info.closureReason = 'Polizeikontrolle'; } // Extract affected vehicles with more detail if (trimmedLine.includes('LKW')) { info.affectedVehicles = 'LKW'; if (trimmedLine.includes('über 3,5 t')) { info.affectedVehicles += ' über 3,5t'; } } else if (trimmedLine.includes('PKW')) { info.affectedVehicles = 'PKW'; } else if (trimmedLine.includes('allen Fahrzeugen') || trimmedLine.includes('beide Richtungen')) { info.affectedVehicles = 'Alle Fahrzeuge'; } else if (trimmedLine.includes('Fahrbahn')) { if (trimmedLine.includes('eine Fahrbahn')) { info.affectedVehicles = 'Eine Fahrbahn'; } else if (trimmedLine.includes('beide Fahrbahnen')) { info.affectedVehicles = 'Beide Fahrbahnen'; } } // Extract direction if specified if (trimmedLine.includes('Richtung ')) { const directionMatch = trimmedLine.match(/Richtung\s+([^\s,]+)/i); if (directionMatch) { info.affectedVehicles += ` (Richtung ${directionMatch[1]})`; } } inRestrictions = false; } else if (trimmedLine.includes('Umleitung') || trimmedLine.includes('Ausweichroute') || trimmedLine.includes('Alternative')) { // Extract detour information info.detour = trimmedLine; inRestrictions = false; } else if (eventType === 'warning' && ( trimmedLine.includes('Stau') || trimmedLine.includes('Verengung') || trimmedLine.includes('Verkehrsführung') || trimmedLine.includes('gesperrt') || trimmedLine.includes('Gefahr') )) { // Traffic situation for warnings info.situation = trimmedLine.replace(/\\n/g, '<br>'); inRestrictions = false; } else if (trimmedLine.includes('Maximale Durchfahrsbreite:') || trimmedLine.includes('Höhenbegrenzung:')) { // Add to restrictions if (info.restrictions) { info.restrictions += '<br>' + trimmedLine; } else { info.restrictions = trimmedLine; } inRestrictions = false; } else if ((eventType === 'closure' || eventType === 'roadworks') && ( trimmedLine.includes('Brücke') || trimmedLine.includes('Tunnel') || trimmedLine.includes('Kreuz') || trimmedLine.includes('Dreieck') || trimmedLine.includes('Anschlussstelle') || trimmedLine.includes('Auffahrt') || trimmedLine.includes('Abfahrt') )) { // Enhanced landmark information for closures info.landmark = trimmedLine; inRestrictions = false; } else if (trimmedLine.includes('AS ') || trimmedLine.includes('AK ') || trimmedLine.includes('AD ')) { // Autobahn junction/interchange codes if (!info.landmark) { info.landmark = trimmedLine; } else { info.landmark += ' • ' + trimmedLine; } inRestrictions = false; } else if (index > 2 && !trimmedLine.includes(':')) { // Additional information (skip first few lines which are usually structured) additionalLines.push(trimmedLine.replace(/\\n/g, '<br>')); inRestrictions = false; } }); if (additionalLines.length > 0) { info.additional = additionalLines.join('<br>'); } return info; } // Calculate section length from extent coordinates function calculateSectionLength(item) { if (!item.extent) { return 0; } try { // Parse extent: "lon1,lat1,lon2,lat2" const extentParts = item.extent.split(',').map(parseFloat); if (extentParts.length !== 4) { return 0; } const [lon1, lat1, lon2, lat2] = extentParts; // Calculate distance between start and end points const distance = calculateDistance(lat1, lon1, lat2, lon2); return distance; } catch (error) { console.error('Error calculating section length:', error); return 0; } } // Format length for display function formatSectionLength(lengthKm) { if (lengthKm === 0) return ''; if (lengthKm < 1) { return `${Math.round(lengthKm * 1000)} m`; } else if (lengthKm < 10) { return `${lengthKm.toFixed(1)} km`; } else { return `${Math.round(lengthKm)} km`; } } // Create visual length indicator function createLengthIndicator(lengthKm, eventType) { if (lengthKm === 0) return ''; // Determine color based on event type and length let color = '#007cba'; let severity = 'normal'; if (eventType === 'closure') { color = '#dc3545'; severity = lengthKm > 5 ? 'severe' : lengthKm > 2 ? 'moderate' : 'normal'; } else if (eventType === 'roadworks') { color = '#ff9800'; severity = lengthKm > 10 ? 'severe' : lengthKm > 5 ? 'moderate' : 'normal'; } else if (eventType === 'warning') { color = '#ffc107'; severity = lengthKm > 3 ? 'moderate' : 'normal'; } // Create visual bar indicator const maxWidth = 120; // Max width in pixels const width = Math.min(maxWidth, Math.max(20, lengthKm * 8)); // Scale factor const severityText = { 'severe': 'Lange Strecke', 'moderate': 'Mittlere Strecke', 'normal': 'Kurze Strecke' }; return ` <div style="margin: 4px 0; padding: 4px; background: rgba(0,0,0,0.05); border-radius: 3px;"> <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 2px;"> <span style="font-size: 10px; font-weight: bold;">Streckenlänge:</span> <span style="font-size: 11px; color: ${color}; font-weight: bold;">${formatSectionLength(lengthKm)}</span> </div> <div style="background: #e0e0e0; height: 8px; border-radius: 4px; overflow: hidden;"> <div style=" background: ${color}; height: 100%; width: ${width}px; border-radius: 4px; transition: width 0.3s ease; background-image: repeating-linear-gradient( 45deg, transparent, transparent 5px, rgba(255,255,255,0.2) 5px, rgba(255,255,255,0.2) 10px ); "></div> </div> <div style="font-size: 9px; color: #666; margin-top: 2px;">${severityText[severity]}</div> </div> `; } // Create tooltip content with full details function createTooltipContent(item) { const typeNames = { roadworks: '🚧 Baustelle', warning: '⚠️ Verkehrsmeldung', closure: '🚫 Sperrung' }; const displayTypeInfo = getDisplayTypeInfo(item.display_type); const typeName = displayTypeInfo.name || typeNames[item.dataType] || item.dataType; const typeIcon = displayTypeInfo.icon || (typeNames[item.dataType] ? typeNames[item.dataType].split(' ')[0] : '📍'); const title = item.title || 'Keine Beschreibung'; const subtitle = item.subtitle || ''; const autobahn = item.autobahn || 'Unbekannt'; const distance = item.distance ? `${item.distance.toFixed(1)} km` : ''; // Extract detailed info from description with enhanced closure parsing let beginInfo = ''; let endInfo = ''; let measureInfo = ''; let restrictionInfo = ''; let additionalInfo = ''; let closedSection = ''; let closureReason = ''; let affectedVehicles = ''; let detour = ''; if (item.description && Array.isArray(item.description)) { const parsedInfo = parseEventDescription(item.description, item.dataType); beginInfo = parsedInfo.begin; endInfo = parsedInfo.end; measureInfo = parsedInfo.measure; restrictionInfo = parsedInfo.restrictions; additionalInfo = parsedInfo.additional; closedSection = parsedInfo.closedSection; closureReason = parsedInfo.closureReason; affectedVehicles = parsedInfo.affectedVehicles; detour = parsedInfo.detour; } // Calculate section length const sectionLength = calculateSectionLength(item); const lengthIndicator = createLengthIndicator(sectionLength, item.dataType); const startTime = item.startTimestamp ? new Date(item.startTimestamp).toLocaleString('de-DE') : ''; return { icon: typeIcon, type: typeName, autobahn: autobahn, title: title, subtitle: subtitle, distance: distance, begin: beginInfo || (startTime ? `Beginn: ${startTime}` : ''), end: endInfo, measure: measureInfo, restrictions: restrictionInfo, additional: additionalInfo, isBlocked: item.isBlocked && item.isBlocked !== 'false', isFuture: item.future, identifier: item.identifier || '', closedSection: closedSection, closureReason: closureReason, affectedVehicles: affectedVehicles, detour: detour, sectionLength: sectionLength, lengthIndicator: lengthIndicator }; } // Show hover tooltip (simple, follows mouse) function showHoverTooltip(event, content) { hideHoverTooltip(); // Clear any existing hover tooltip const tooltip = document.createElement('div'); tooltip.id = 'traffic-hover-tooltip'; tooltip.style.cssText = ` position: fixed; background: rgba(0, 0, 0, 0.8); color: white; padding: 8px 12px; border-radius: 4px; font-size: 12px; font-family: Arial, sans-serif; z-index: 10002; pointer-events: none; max-width: 300px; line-height: 1.3; `; tooltip.innerHTML = ` <div style="font-weight: bold; margin-bottom: 4px;"> ${content.icon} ${content.type} - ${content.autobahn} </div> <div style="font-size: 11px;"> ${content.title} </div> ${content.distance ? `<div style="font-size: 10px; margin-top: 4px; opacity: 0.8;">Entfernung: ${content.distance}</div>` : ''} ${content.sectionLength > 0 ? `<div style="font-size: 10px; margin-top: 2px; opacity: 0.9; color: #ffc107;">Länge: ${formatSectionLength(content.sectionLength)}</div>` : ''} `; document.body.appendChild(tooltip); currentTooltip = tooltip; // Position tooltip const x = event.clientX + 15; const y = event.clientY - 10; tooltip.style.left = x + 'px'; tooltip.style.top = y + 'px'; // Adjust if tooltip goes off screen setTimeout(() => { const rect = tooltip.getBoundingClientRect(); if (rect.right > window.innerWidth) { tooltip.style.left = (event.clientX - rect.width - 15) + 'px'; } if (rect.bottom > window.innerHeight) { tooltip.style.top = (event.clientY - rect.height - 15) + 'px'; } }, 10); } // Hide hover tooltip function hideHoverTooltip() { if (currentTooltip) { currentTooltip.remove(); currentTooltip = null; } } // Show persistent popup with full details (on click) function showPersistentPopup(event, content) { // Don't create multiple popups for the same event const existingPopup = document.getElementById(`traffic-popup-${content.identifier}`); if (existingPopup) { // Just bring existing popup to front existingPopup.style.zIndex = '10001'; return; } const popup = document.createElement('div'); popup.id = `traffic-popup-${content.identifier}`; popup.className = 'traffic-persistent-popup'; popup.style.cssText = ` position: fixed; background: white; border: 2px solid #007cba; border-radius: 8px; padding: 12px; font-size: 12px; font-family: Arial, sans-serif; box-shadow: 0 6px 20px rgba(0,0,0,0.3); z-index: 10000; max-width: 400px; min-width: 280px; opacity: 0; transition: opacity 0.2s ease; line-height: 1.4; user-select: text; cursor: default; `; // Create close button const closeButton = document.createElement('div'); closeButton.innerHTML = '✕'; closeButton.style.cssText = ` position: absolute; top: 8px; right: 8px; width: 20px; height: 20px; background: #ff4444; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-weight: bold; font-size: 12px; z-index: 1; `; closeButton.addEventListener('click', function(e) { e.stopPropagation(); popup.remove(); }); // Create drag handle const dragHandle = document.createElement('div'); dragHandle.innerHTML = '⋮⋮'; dragHandle.style.cssText = ` position: absolute; top: 8px; right: 32px; width: 20px; height: 20px; background: #007cba; color: white; border-radius: 3px; display: flex; align-items: center; justify-content: center; cursor: move; font-weight: bold; font-size: 10px; z-index: 1; `; let popupHTML = ` <div style="margin-bottom: 8px; font-weight: bold; color: #007cba; border-bottom: 1px solid #eee; padding-bottom: 6px; padding-right: 60px; font-size: 13px;"> ${content.icon} ${content.type} - ${content.autobahn} </div> <div style="margin-bottom: 6px;"> <strong>📍 Strecke:</strong><br> <span style="font-size: 11px; user-select: text;">${content.title}</span> </div> `; if (content.subtitle) { popupHTML += ` <div style="margin-bottom: 6px;"> <strong>🚗 Richtung:</strong> <span style="font-size: 11px; user-select: text;">${content.subtitle}</span> </div>`; } if (content.measure) { popupHTML += ` <div style="margin-bottom: 6px;"> <strong>🔧 Maßnahme:</strong> <span style="font-size: 11px; user-select: text;">${content.measure}</span> </div>`; } // Enhanced closure information display if (content.closedSection) { popupHTML += ` <div style="margin-bottom: 6px; padding: 6px; background: #fff3cd; border-left: 3px solid #dc3545; border-radius: 3px;"> <strong>🚧 Gesperrter Streckenabschnitt:</strong><br> <span style="font-size: 12px; font-weight: bold; color: #721c24; user-select: text;">${content.closedSection}</span> </div>`; } // Add visual length indicator if (content.lengthIndicator) { popupHTML += content.lengthIndicator; } if (content.closureReason) { popupHTML += ` <div style="margin-bottom: 6px;"> <strong>❓ Sperrungsgrund:</strong> <span style="font-size: 11px; user-select: text;">${content.closureReason}</span> </div>`; } if (content.affectedVehicles) { popupHTML += ` <div style="margin-bottom: 6px;"> <strong>🚛 Betroffene Fahrzeuge:</strong> <span style="font-size: 11px; user-select: text;">${content.affectedVehicles}</span> </div>`; } if (content.detour) { popupHTML += ` <div style="margin-bottom: 6px; padding: 6px; background: #d4edda; border-left: 3px solid #28a745; border-radius: 3px;"> <strong>🛣️ Umleitung:</strong><br> <span style="font-size: 11px; user-select: text;">${content.detour}</span> </div>`; } if (content.begin || content.end) { popupHTML += ` <div style="margin-bottom: 6px;"> <strong>📅 Zeitraum:</strong><br> <span style="font-size: 11px; user-select: text;"> ${content.begin}<br> ${content.end || 'Ende: Unbekannt'} </span> </div>`; } if (content.restrictions) { popupHTML += ` <div style="margin-bottom: 6px; padding: 6px; background: #fff3cd; border-left: 3px solid #ffc107; border-radius: 3px;"> <strong>⚠️ Einschränkungen:</strong><br> <span style="font-size: 11px; user-select: text;">${content.restrictions}</span> </div>`; } if (content.additional) { popupHTML += ` <div style="margin-bottom: 6px; padding: 6px; background: #e8f4fd; border-left: 3px solid #007cba; border-radius: 3px;"> <strong>ℹ️ Information:</strong><br> <span style="font-size: 11px; user-select: text;">${content.additional}</span> </div>`; } if (content.isFuture) { popupHTML += ` <div style="margin-bottom: 6px; padding: 4px; background: #d1ecf1; border-radius: 3px; font-size: 11px; color: #0c5460;"> <strong>🔮 Zukünftiges Ereignis</strong> </div>`; } if (content.isBlocked) { popupHTML += ` <div style="margin-bottom: 6px; padding: 4px; background: #f8d7da; border-radius: 3px; font-size: 11px; color: #721c24;"> <strong>🚫 Blockiert</strong> </div>`; } if (content.distance) { popupHTML += ` <div style="margin-bottom: 6px;"> <strong>📏 Entfernung:</strong> <span style="user-select: text;">${content.distance}</span> </div>`; } popupHTML += ` <div style="font-size: 10px; color: #666; border-top: 1px solid #eee; padding-top: 6px; margin-top: 8px;"> <strong>ID:</strong> <span style="user-select: text;">${content.identifier}</span> </div> <div style="margin-top: 8px; text-align: center;"> <button class="copy-id-btn" style="padding: 4px 8px; font-size: 10px; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer; margin-right: 5px;"> 📋 Copy ID </button> <button class="copy-all-btn" style="padding: 4px 8px; font-size: 10px; background: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer;"> 📄 Copy All Info </button> </div> `; popup.innerHTML = popupHTML; popup.appendChild(closeButton); popup.appendChild(dragHandle); // Add event listeners for copy buttons const copyIdBtn = popup.querySelector('.copy-id-btn'); const copyAllBtn = popup.querySelector('.copy-all-btn'); copyIdBtn.addEventListener('click', function() { copyToClipboard(content.identifier, popup); }); copyAllBtn.addEventListener('click', function() { copyAllInfo(content, popup); }); document.body.appendChild(popup); // Position popup let x = event.clientX + 15; let y = event.clientY - 10; popup.style.left = x + 'px'; popup.style.top = y + 'px'; // Show with fade-in setTimeout(() => { popup.style.opacity = '1'; }, 10); // Adjust position if popup goes off screen setTimeout(() => { const rect = popup.getBoundingClientRect(); if (rect.right > window.innerWidth) { x = event.clientX - rect.width - 15; popup.style.left = x + 'px'; } if (rect.bottom > window.innerHeight) { y = event.clientY - rect.height - 10; popup.style.top = y + 'px'; } if (rect.top < 0) { popup.style.top = '10px'; } if (x < 0) { popup.style.left = '10px'; } }, 20); // Make popup draggable makeDraggable(popup, dragHandle); // Store content for copy functions popup.contentData = content; } // Hide tooltip (now hides all popups and hover tooltips) function hideTooltip() { hideHoverTooltip(); const popups = document.querySelectorAll('.traffic-persistent-popup'); popups.forEach(popup => popup.remove()); } // Make popup draggable function makeDraggable(popup, handle) { let isDragging = false; let startX, startY; let popupStartX, popupStartY; handle.addEventListener('mousedown', dragStart); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', dragEnd); function dragStart(e) { e.preventDefault(); isDragging = true; // Get the current popup position const rect = popup.getBoundingClientRect(); popupStartX = rect.left; popupStartY = rect.top; // Get mouse position startX = e.clientX; startY = e.clientY; popup.style.zIndex = '10001'; // Bring to front when dragging popup.style.transition = 'none'; // Disable transition during drag // Add dragging class for visual feedback popup.style.opacity = '0.9'; handle.style.background = '#005a9b'; } function drag(e) { if (!isDragging) return; e.preventDefault(); // Calculate the new position const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; const newX = popupStartX + deltaX; const newY = popupStartY + deltaY; // Apply constraints to keep popup on screen const maxX = window.innerWidth - popup.offsetWidth; const maxY = window.innerHeight - popup.offsetHeight; const constrainedX = Math.max(0, Math.min(newX, maxX)); const constrainedY = Math.max(0, Math.min(newY, maxY)); popup.style.left = constrainedX + 'px'; popup.style.top = constrainedY + 'px'; } function dragEnd(e) { if (!isDragging) return; isDragging = false; // Restore visual feedback popup.style.opacity = '1'; popup.style.transition = 'opacity 0.2s ease'; handle.style.background = '#007cba'; } } // Copy functions function copyToClipboard(identifier, popup) { navigator.clipboard.writeText(identifier).then(() => { // Show brief confirmation if (popup) { const originalBorder = popup.style.border; popup.style.border = '2px solid #28a745'; setTimeout(() => { popup.style.border = originalBorder; }, 300); } }).catch(err => { console.error('Failed to copy ID:', err); // Fallback for older browsers const textArea = document.createElement('textarea'); textArea.value = identifier; document.body.appendChild(textArea); textArea.select(); try { document.execCommand('copy'); if (popup) { const originalBorder = popup.style.border; popup.style.border = '2px solid #28a745'; setTimeout(() => { popup.style.border = originalBorder; }, 300); } } catch (fallbackErr) { console.error('Fallback copy failed:', fallbackErr); } document.body.removeChild(textArea); }); } function copyAllInfo(content, popup) { let copyText = `${content.type} - ${content.autobahn}\n`; copyText += `Strecke: ${content.title}\n`; if (content.subtitle) copyText += `Richtung: ${content.subtitle}\n`; if (content.measure) copyText += `Maßnahme: ${content.measure}\n`; if (content.begin) copyText += `${content.begin}\n`; if (content.end) copyText += `${content.end}\n`; if (content.restrictions) copyText += `Einschränkungen: ${content.restrictions}\n`; if (content.additional) copyText += `Information: ${content.additional}\n`; if (content.distance) copyText += `Entfernung: ${content.distance}\n`; copyText += `ID: ${content.identifier}`; navigator.clipboard.writeText(copyText).then(() => { // Show brief confirmation if (popup) { const originalBorder = popup.style.border; popup.style.border = '2px solid #28a745'; setTimeout(() => { popup.style.border = originalBorder; }, 300); } }).catch(err => { console.error('Failed to copy info:', err); // Fallback for older browsers const textArea = document.createElement('textarea'); textArea.value = copyText; document.body.appendChild(textArea); textArea.select(); try { document.execCommand('copy'); if (popup) { const originalBorder = popup.style.border; popup.style.border = '2px solid #28a745'; setTimeout(() => { popup.style.border = originalBorder; }, 300); } } catch (fallbackErr) { console.error('Fallback copy failed:', fallbackErr); } document.body.removeChild(textArea); }); } // Create marker icon based on data type function createMarkerIcon(dataType) { const iconConfig = { roadworks: { emoji: '🚧', color: '#ff9800' }, warning: { emoji: '⚠️', color: '#f44336' }, closure: { emoji: '🚫', color: '#9c27b0' } }; const config = iconConfig[dataType] || { emoji: '📍', color: '#2196f3' }; const svg = ` <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"> <circle cx="12" cy="12" r="10" fill="${config.color}" stroke="white" stroke-width="2" opacity="0.9"/> <text x="12" y="16" text-anchor="middle" font-size="12" fill="white">${config.emoji}</text> </svg> `; return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg); } // Get display type specific information function getDisplayTypeInfo(displayType) { const displayTypes = { 'ROADWORKS': { name: 'Baustelle', icon: '🚧', description: 'Bauarbeiten im Gange', color: '#fff3cd' }, 'WEBCAM': { name: 'Webcam', icon: '📹', description: 'Verkehrskamera verfügbar', color: '#e8f4fd' }, 'PARKING': { name: 'Rastplatz', icon: '🅿️', description: 'Parkplatz/Rastanlage', color: '#d4edda' }, 'WARNING': { name: 'Verkehrsmeldung', icon: '⚠️', description: 'Aktuelle Verkehrswarnung', color: '#fff3cd' }, 'WEIGHT_LIMIT_35': { name: 'Gewichtsbeschränkung', icon: '⚖️', description: 'Sperrung für Fahrzeuge über 3,5t', color: '#f8d7da' }, 'CLOSURE': { name: 'Sperrung', icon: '🚫', description: 'Vollsperrung der Fahrbahn', color: '#f8d7da' }, 'CLOSURE_ENTRY_EXIT': { name: 'Anschlussstellen-Sperrung', icon: '🚫', description: 'Ein-/Ausfahrt gesperrt', color: '#f8d7da' }, 'STRONG_ELECTRIC_CHARGING_STATION': { name: 'Schnellladestation', icon: '⚡', description: 'Elektrische Schnellladestation', color: '#d1ecf1' } }; return displayTypes[displayType] || {}; } // Add a road section line based on extent data with length labels function addRoadSection(item) { if (!item.extent || !vectorLayer) { return; } try { // Parse extent: "lon1,lat1,lon2,lat2" const extentParts = item.extent.split(',').map(parseFloat); if (extentParts.length !== 4) { console.log('Invalid extent format for item:', item.identifier); return; } const [lon1, lat1, lon2, lat2] = extentParts; // Create line geometry from extent corners const points = [ new OpenLayers.Geometry.Point(lon1, lat1).transform( new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:3857") ), new OpenLayers.Geometry.Point(lon2, lat2).transform( new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:3857") ) ]; const lineGeometry = new OpenLayers.Geometry.LineString(points); // Calculate section length for display const sectionLength = calculateSectionLength(item); const lengthText = formatSectionLength(sectionLength); // Create style based on event type const style = getRoadSectionStyle(item.dataType); const lineFeature = new OpenLayers.Feature.Vector(lineGeometry, { eventId: item.identifier, eventType: item.dataType, title: item.title, autobahn: item.autobahn, sectionLength: lengthText }, style); // Store item data for events lineFeature.eventData = item; vectorLayer.addFeatures([lineFeature]); trafficFeatures.push({feature: lineFeature, data: item}); // Add length label as separate feature if length is available if (sectionLength > 0) { addSectionLengthLabel(lineGeometry, lengthText, item.dataType, item.identifier); } console.log('Added road section for:', item.identifier, 'extent:', item.extent, 'length:', lengthText); } catch (error) { console.error('Error adding road section for item:', item.identifier, error); } } // Add length label on the road section using OpenLayers 2 compatible method function addSectionLengthLabel(lineGeometry, lengthText, eventType, identifier) { try { // Calculate midpoint of the line const bounds = lineGeometry.getBounds(); const centerPoint = bounds.getCenterLonLat(); // Create point geometry for label const labelPoint = new OpenLayers.Geometry.Point(centerPoint.lon, centerPoint.lat); // Style for the length label - simplified for OpenLayers 2 const labelStyle = new OpenLayers.Style({ label: lengthText, fontColor: "#ffffff", fontSize: "11px", fontFamily: "Arial, sans-serif", fontWeight: "bold", labelAlign: "cm", labelXOffset: 0, labelYOffset: 0, labelOutlineColor: "#000000", labelOutlineWidth: 2, pointRadius: 8, fillColor: getEventTypeColor(eventType), fillOpacity: 0.9, strokeColor: "#ffffff", strokeWidth: 1, strokeOpacity: 1 }); const labelFeature = new OpenLayers.Feature.Vector(labelPoint, { eventId: identifier + '_label', isLabel: true, lengthText: lengthText }, labelStyle); vectorLayer.addFeatures([labelFeature]); trafficFeatures.push({feature: labelFeature, data: {identifier: identifier + '_label', isLabel: true}}); console.log('Added length label:', lengthText, 'for event:', identifier); } catch (error) { console.error('Error adding section label:', error); } } // Get event type color for labels function getEventTypeColor(eventType) { const colors = { roadworks: "#ff9800", warning: "#f44336", closure: "#9c27b0" }; return colors[eventType] || "#2196f3"; } // Get style for road section based on event type function getRoadSectionStyle(dataType) { const styleConfig = { roadworks: { strokeColor: "#ff9800", strokeWidth: 6, strokeOpacity: 0.8, strokeDasharray: "15,10" }, warning: { strokeColor: "#f44336", strokeWidth: 5, strokeOpacity: 0.7, strokeDasharray: "10,5" }, closure: { strokeColor: "#9c27b0", strokeWidth: 7, strokeOpacity: 0.9, strokeDasharray: "20,5" } }; const config = styleConfig[dataType] || { strokeColor: "#2196f3", strokeWidth: 4, strokeOpacity: 0.6, strokeDasharray: "8,8" }; return config; // Return plain style object } // Clear all vector features (road sections) function clearFeatures() { if (vectorLayer) { vectorLayer.removeAllFeatures(); } trafficFeatures = []; } // Clear all markers function clearMarkers() { if (overlayLayer) { overlayLayer.clearMarkers(); } trafficMarkers = []; } // Get selected data sources function getSelectedSources() { const sources = []; document.querySelectorAll('.vz-source:checked').forEach(checkbox => { sources.push(checkbox.dataset.source); }); return sources; } // Update status display function updateStatus(message) { const statusDisplay = document.getElementById('statusDisplay'); if (statusDisplay) { const timestamp = new Date().toLocaleTimeString('de-DE'); statusDisplay.innerHTML = `[${timestamp}] ${message}`; } console.log(`${SCRIPT_NAME}: ${message}`); } // Update statistics display function updateStatistics() { const statisticsDisplay = document.getElementById('statisticsDisplay'); if (statisticsDisplay) { const lastUpdate = lastUpdateTime ? lastUpdateTime.toLocaleString('de-DE') : 'Never'; const totalEvents = trafficMarkers.length; const activeRequests = currentRequests.length; statisticsDisplay.innerHTML = ` <strong>Statistics:</strong><br> Last Update: ${lastUpdate}<br> Total Events: ${totalEvents}<br> Active Requests: ${activeRequests} `; } } // Main initialization logic function bootstrap() { if (W?.userscripts?.state.isReady) { initializeScript(); } else if (W?.userscripts?.state.isInitialized) { document.addEventListener("wme-ready", initializeScript, { once: true }); } else { document.addEventListener("wme-initialized", function() { if (W?.userscripts?.state.isReady) { initializeScript(); } else { document.addEventListener("wme-ready", initializeScript, { once: true }); } }, { once: true }); } } // Start the script bootstrap(); // Cleanup function window.addEventListener('beforeunload', function() { try { if (updateInterval) { clearInterval(updateInterval); } currentRequests.forEach(request => { if (request.abort) request.abort(); }); if (overlayLayer && W.map) { W.map.removeLayer(overlayLayer); } if (vectorLayer && W.map) { W.map.removeLayer(vectorLayer); } hideTooltip(); // Clean up any tooltips W.userscripts.removeSidebarTab(SCRIPT_ID); } catch (error) { console.error('Cleanup error:', error); } }); })();