Greasy Fork is available in English.

CGN Airport Busiest Flight Window Analyzer

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);