您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Analyzes flight data from Cologne Bonn Airport (CGN) to find the busiest time windows for plane spotting, based on user-specified date and time range and maximum window duration.
// ==UserScript== // @name CGN Airport Busiest Flight Window Analyzer // @namespace shiftgeist // @match https://www.koeln-bonn-airport.de/fluggaeste/fluege/abflug-ankunft.html // @version 20250430 // @author shiftgeist // @description Analyzes flight data from Cologne Bonn Airport (CGN) to find the busiest time windows for plane spotting, based on user-specified date and time range and maximum window duration. // @license GNU GPLv3 // @icon https://www.google.com/s2/favicons?sz=64&domain=www.koeln-bonn-airport.de // ==/UserScript== const debug = window.localStorage.getItem('debug-log') === 'true'; const doc = document; const elAttach = '#main-content-container'; let statsDisplay = null; let timeoutCounter = 0; const jsonEndpointUrl = 'https://www.koeln-bonn-airport.de/fluggaeste/fluege/abflug-ankunft/fsjson'; function log(...params) { if (debug) { console.debug('[Traffic]', ...params); } } function timeToMinutes(timeStr) { const [hours, minutes] = timeStr.split(':').map(Number); return hours * 60 + minutes; } function minutesToTime(totalMinutes) { const hours = Math.floor(totalMinutes / 60) % 24; const minutes = totalMinutes % 60; const hourStr = String(hours).padStart(2, '0'); const minuteStr = String(minutes).padStart(2, '0'); return `${hourStr}:${minuteStr}`; } function findBusiestWindowInRange(flightTimes, maxDurationMinutes) { if (!flightTimes || flightTimes.length < 2) { return null; } const flightMinutes = flightTimes.map(timeToMinutes).sort((a, b) => a - b); let bestWindow = { startTime: -1, endTime: -1, count: 0, duration: Infinity }; for (let i = 0; i < flightMinutes.length; i++) { const currentStartTime = flightMinutes[i]; for (let j = i + 1; j < flightMinutes.length; j++) { const currentEndTime = flightMinutes[j]; const currentDuration = currentEndTime - currentStartTime; if (currentDuration <= maxDurationMinutes) { const currentCount = j - i + 1; if (currentCount > bestWindow.count) { bestWindow.count = currentCount; bestWindow.duration = currentDuration; bestWindow.startTime = currentStartTime; bestWindow.endTime = currentEndTime; } else if (currentCount === bestWindow.count) { if (currentDuration < bestWindow.duration) { bestWindow.duration = currentDuration; bestWindow.startTime = currentStartTime; bestWindow.endTime = currentEndTime; } } } if (currentDuration > maxDurationMinutes) { break; } } } if (bestWindow.count > 0) { return { window: `${minutesToTime(bestWindow.startTime)} - ${minutesToTime(bestWindow.endTime)}`, count: bestWindow.count, durationMinutes: bestWindow.duration }; } else { return null; } } function findFlightWindows(flightTimes, maxDurationMinutes) { return findBusiestWindowInRange(flightTimes, maxDurationMinutes); } function getCurrentDateFormatted() { const now = new Date(); const day = String(now.getDate()).padStart(2, '0'); const month = String(now.getMonth() + 1).padStart(2, '0'); const year = now.getFullYear(); return `${day}.${month}.${year}`; } function getCurrentDateInputFormatted() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } function getCurrentTimeFormatted() { const now = new Date(); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); return `${hours}:${minutes}`; } function buildJsonPostData(startDate, startTime, endDate, endTime) { const startDateTime = new Date(`${startDate.split('.').reverse().join('-')}T${startTime}:00`); const endDateTime = new Date(`${endDate.split('.').reverse().join('-')}T${endTime}:00`); const startTimestamp = Math.floor(startDateTime.getTime() / 1000); const endTimestamp = Math.floor(endDateTime.getTime() / 1000); return `mode=A&mode=D&more=&flightsperpage=500&tolerance=&page=0&dtpSTARTDATE=${startDate}+${startTime}&START=${startTimestamp}&END=${endTimestamp}&destination=&datehelper=${startDate}+${startTime}&date=${startDate}+${startTime}`; } async function fetchFlightData(postData, callback) { log('Fetching flight data from JSON endpoint with data:', postData); const statsOutputDiv = doc.getElementById('stats-output'); if (statsOutputDiv) { statsOutputDiv.innerHTML = 'Fetching flight data...'; statsDisplay.style.display = 'flex'; } try { const response = await fetch(jsonEndpointUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: postData }); if (!response.ok) { log(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`); } const jsonResponse = await response.json(); log('JSON response received:', jsonResponse); const flightTimes = []; if (jsonResponse && Array.isArray(jsonResponse.flights)) { jsonResponse.flights.forEach(flight => { const time = (flight.expected && flight.expected !== flight.time) ? flight.expected : flight.time; if (time) { flightTimes.push(time.trim()); } }); } log('Extracted flight times:', flightTimes); callback(flightTimes); } catch (e) { log('Error fetching or processing JSON data:', e); const statsOutputDiv = doc.getElementById('stats-output'); if (statsOutputDiv) { statsOutputDiv.innerHTML = `Error: ${e.message || 'Could not fetch flight data.'}`; statsDisplay.style.display = 'flex'; } } } function createStats() { const container = doc.querySelector(elAttach); if (!container) { log('Attachment container not found:', elAttach); return; } if (!statsDisplay) { statsDisplay = doc.createElement('div'); statsDisplay.id = 'plane-spotting-stats'; statsDisplay.style.cssText = ` position: fixed; top: 10px; right: 10px; background-color: rgba(0, 0, 0, 0.7); color: white; padding: 10px; border-radius: 5px; z-index: 10000; font-family: sans-serif; font-size: 14px; min-width: 300px; display: flex; flex-direction: column; gap: 5px; `; doc.body.appendChild(statsDisplay); const currentDateFormatted = getCurrentDateFormatted(); const currentDateInputFormatted = getCurrentDateInputFormatted(); const currentTime = getCurrentTimeFormatted(); const endOfDayTime = '23:59'; const defaultMaxDuration = 180; // Default max duration in minutes (3 hours) statsDisplay.innerHTML = ` <div> <strong>Time Range:</strong> </div> <div style="display: flex; gap: 5px;"> <label for="earliest-date">From:</label> <input type="date" id="earliest-date" value="${currentDateInputFormatted}"> <input type="time" id="earliest-time" value="${currentTime}"> </div> <div style="display: flex; gap: 5px;"> <label for="latest-date">To:</label> <input type="date" id="latest-date" value="${currentDateInputFormatted}"> <input type="time" id="latest-time" value="${endOfDayTime}"> </div> <div> <label for="max-duration">Max Window Duration (mins):</label> <input type="number" id="max-duration" value="${defaultMaxDuration}" min="1"> </div> <button id="fetch-flights-button">Analyze Flights</button> <div id="stats-output"></div> `; doc.getElementById('fetch-flights-button').addEventListener('click', () => { const earliestDate = doc.getElementById('earliest-date').value; const earliestTime = doc.getElementById('earliest-time').value; const latestDate = doc.getElementById('latest-date').value; const latestTime = doc.getElementById('latest-time').value; const maxDuration = parseInt(doc.getElementById('max-duration').value, 10); if (earliestDate && earliestTime && latestDate && latestTime && !isNaN(maxDuration) && maxDuration > 0) { const startDateFormatted = earliestDate.split('-').reverse().join('.'); const endDateFormatted = latestDate.split('-').reverse().join('.'); const postData = buildJsonPostData(startDateFormatted, earliestTime, endDateFormatted, latestTime); fetchFlightData(postData, (flightTimes) => { const busiestWindow = findFlightWindows(flightTimes, maxDuration); log('Busiest window data:', busiestWindow); updateStatsDisplay(busiestWindow); }); } else { updateStatsDisplay(null); } }); const initialPostData = buildJsonPostData(currentDateFormatted, currentTime, currentDateFormatted, endOfDayTime); fetchFlightData(initialPostData, (flightTimes) => { const busiestWindow = findFlightWindows(flightTimes, defaultMaxDuration); log('Initial busiest window data:', busiestWindow); updateStatsDisplay(busiestWindow); }); } else { log('Stats display already exists, triggering data fetch based on current inputs.'); const earliestDate = doc.getElementById('earliest-date').value; const earliestTime = doc.getElementById('earliest-time').value; const latestDate = doc.getElementById('latest-date').value; const latestTime = doc.getElementById('latest-time').value; const maxDuration = parseInt(doc.getElementById('max-duration').value, 10); if (earliestDate && earliestTime && latestDate && latestTime && !isNaN(maxDuration) && maxDuration > 0) { const startDateFormatted = earliestDate.split('-').reverse().join('.'); const endDateFormatted = latestDate.split('-').reverse().join('.'); const postData = buildJsonPostData(startDateFormatted, earliestTime, endDateFormatted, latestTime); fetchFlightData(postData, (flightTimes) => { const busiestWindow = findFlightWindows(flightTimes, maxDuration); log('Busiest window data (update):', busiestWindow); updateStatsDisplay(busiestWindow); }); } else { updateStatsDisplay(null); } } } function updateStatsDisplay(busiestWindow) { const statsOutputDiv = doc.getElementById('stats-output'); if (statsOutputDiv) { if (busiestWindow) { statsOutputDiv.innerHTML = ` <strong>Busiest Window:</strong> ${busiestWindow.window}<br> <strong>Flights:</strong> ${busiestWindow.count}<br> <strong>Duration:</strong> ${busiestWindow.durationMinutes} mins `; } else { statsOutputDiv.innerHTML = 'No suitable busy window found in the specified range.'; } statsDisplay.style.display = 'flex'; } } function waitForAttachElement(callback) { log('Waiting for attachment element...'); const container = doc.querySelector(elAttach); if (container) { log('Attachment element found.'); callback(); } else { timeoutCounter += 1; const delay = 20 * (timeoutCounter / 2 + 1); log(`Attachment element not found, retrying in ${delay}ms. Attempt ${timeoutCounter}`); if (timeoutCounter < 50) { setTimeout(() => waitForAttachElement(callback), delay); } else { log('Max waitForAttachElement attempts reached. Could not find necessary elements.'); if (!statsDisplay) { statsDisplay = doc.createElement('div'); statsDisplay.style.cssText = ` position: fixed; top: 10px; right: 10px; background-color: rgba(255, 99, 71, 0.8); color: white; padding: 10px; border-radius: 5px; z-index: 10000; font-family: sans-serif; font-size: 14px; `; statsDisplay.innerHTML = 'Plane Spotting script could not find the attachment element.'; doc.body.appendChild(statsDisplay); } } } } waitForAttachElement(createStats);