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