Initial commit: Digital signage system for transit departures, weather, and news ticker
This commit is contained in:
815
departures.js
Normal file
815
departures.js
Normal file
@@ -0,0 +1,815 @@
|
||||
// Calculate minutes until arrival
|
||||
function calculateMinutesUntilArrival(scheduledTime) {
|
||||
const now = new Date();
|
||||
const scheduled = new Date(scheduledTime);
|
||||
return Math.round((scheduled - now) / (1000 * 60));
|
||||
}
|
||||
|
||||
// Get transport icon based on transport mode
|
||||
function getTransportIcon(transportMode) {
|
||||
// Default to bus if not specified
|
||||
const mode = transportMode ? transportMode.toLowerCase() : 'bus';
|
||||
|
||||
// Special case for line 7 - it's a tram
|
||||
if (arguments.length > 1 && arguments[1] && arguments[1].designation === '7') {
|
||||
return '<svg class="transport-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M13 5l.75-1.5H17V2H7v1.5h4.75L11 5c-3.13.09-6 .73-6 3.5V17c0 1.5 1.11 2.73 2.55 2.95L6 21.5v.5h2l2-2h4l2 2h2v-.5l-1.55-1.55C17.89 19.73 19 18.5 19 17V8.5c0-2.77-2.87-3.41-6-3.5zm-1 13.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm5-4.5H7V9h10v5z"/></svg>';
|
||||
}
|
||||
|
||||
// SVG icons for different transport modes
|
||||
const icons = {
|
||||
bus: '<svg class="transport-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M4 16c0 .88.39 1.67 1 2.22V20c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h8v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1.78c.61-.55 1-1.34 1-2.22V6c0-3.5-3.58-4-8-4s-8 .5-8 4v10zm3.5 1c-.83 0-1.5-.67-1.5-1.5S6.67 14 7.5 14s1.5.67 1.5 1.5S8.33 17 7.5 17zm9 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm1.5-6H6V6h12v5z"/></svg>',
|
||||
metro: '<svg class="transport-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M12 2c-4 0-8 .5-8 4v9.5C4 17.43 5.57 19 7.5 19L6 20v1h12v-1l-1.5-1c1.93 0 3.5-1.57 3.5-3.5V6c0-3.5-4-4-8-4zm0 2c3.51 0 4.96.48 5.57 1H6.43c.61-.52 2.06-1 5.57-1zM6 7h5v3H6V7zm12 0v3h-5V7h5zm-6 5v3H6v-3h6zm1 0h5v3h-5v-3z"/></svg>',
|
||||
train: '<svg class="transport-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M12 2c-4 0-8 .5-8 4v9.5C4 17.43 5.57 19 7.5 19L6 20v1h12v-1l-1.5-1c1.93 0 3.5-1.57 3.5-3.5V6c0-3.5-4-4-8-4zm0 2c3.51 0 4.96.48 5.57 1H6.43c.61-.52 2.06-1 5.57-1zM6 7h5v3H6V7zm12 0v3h-5V7h5zm-6 5v3H6v-3h6zm1 0h5v3h-5v-3z"/></svg>',
|
||||
tram: '<svg class="transport-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M13 5l.75-1.5H17V2H7v1.5h4.75L11 5c-3.13.09-6 .73-6 3.5V17c0 1.5 1.11 2.73 2.55 2.95L6 21.5v.5h2l2-2h4l2 2h2v-.5l-1.55-1.55C17.89 19.73 19 18.5 19 17V8.5c0-2.77-2.87-3.41-6-3.5zm-1 13.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm5-4.5H7V9h10v5z"/></svg>',
|
||||
ship: '<svg class="transport-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M20 21c-1.39 0-2.78-.47-4-1.32-2.44 1.71-5.56 1.71-8 0C6.78 20.53 5.39 21 4 21H2v2h2c1.38 0 2.74-.35 4-.99 2.52 1.29 5.48 1.29 8 0 1.26.65 2.62.99 4 .99h2v-2h-2zM3.95 19H4c1.6 0 3.02-.88 4-2 .98 1.12 2.4 2 4 2s3.02-.88 4-2c.98 1.12 2.4 2 4 2h.05l1.89-6.68c.08-.26.06-.54-.06-.78s-.34-.42-.6-.5L20 10.62V6c0-1.1-.9-2-2-2h-3V1H9v3H6c-1.1 0-2 .9-2 2v4.62l-1.29.42c-.26.08-.48.26-.6.5s-.15.52-.06.78L3.95 19zM6 6h12v3.97L12 8 6 9.97V6z"/></svg>'
|
||||
};
|
||||
|
||||
return icons[mode] || icons.bus;
|
||||
}
|
||||
|
||||
// Create a departure card element
|
||||
function createDepartureCard(departure) {
|
||||
const departureCard = document.createElement('div');
|
||||
departureCard.dataset.journeyId = departure.journey.id;
|
||||
|
||||
const displayTime = departure.display;
|
||||
const scheduledTime = formatDateTime(departure.scheduled);
|
||||
|
||||
// Check if departure is within the next hour
|
||||
const departureTime = new Date(departure.scheduled);
|
||||
const now = new Date();
|
||||
const diffMinutes = Math.round((departureTime - now) / (1000 * 60));
|
||||
const isWithinNextHour = diffMinutes <= 60;
|
||||
|
||||
// Add condensed class if within next hour
|
||||
departureCard.className = isWithinNextHour ? 'departure-card condensed' : 'departure-card';
|
||||
|
||||
// Check if the display time is just a time (HH:MM) or a countdown
|
||||
const isTimeOnly = /^\d{1,2}:\d{2}$/.test(displayTime);
|
||||
|
||||
// If it's just a time, calculate minutes until arrival
|
||||
let countdownText = displayTime;
|
||||
if (isTimeOnly) {
|
||||
const minutesUntil = calculateMinutesUntilArrival(departure.scheduled);
|
||||
if (minutesUntil <= 0) {
|
||||
countdownText = 'Now';
|
||||
} else if (minutesUntil === 1) {
|
||||
countdownText = '1 min';
|
||||
} else {
|
||||
countdownText = `${minutesUntil} min`;
|
||||
}
|
||||
}
|
||||
|
||||
// Get transport icon based on transport mode and line
|
||||
const transportIcon = getTransportIcon(departure.line?.transportMode, departure.line);
|
||||
|
||||
// Create card based on time and display format
|
||||
departureCard.innerHTML = `
|
||||
<div class="departure-header">
|
||||
<span class="line-number">
|
||||
${transportIcon}
|
||||
${departure.line.designation}
|
||||
<span class="line-destination">${departure.destination}</span>
|
||||
</span>
|
||||
<span class="time">
|
||||
<span class="arrival-time">${scheduledTime}</span>
|
||||
<span class="countdown">(${countdownText})</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return departureCard;
|
||||
}
|
||||
|
||||
// Display departures grouped by line number
|
||||
function displayGroupedDeparturesByLine(groups, container) {
|
||||
groups.forEach(group => {
|
||||
// Create a card for this line number
|
||||
const groupCard = document.createElement('div');
|
||||
groupCard.className = 'departure-card line-card';
|
||||
|
||||
// Create card header
|
||||
const header = document.createElement('div');
|
||||
header.className = 'departure-header';
|
||||
|
||||
// Get transport icon based on transport mode and line
|
||||
const transportIcon = getTransportIcon(group.line?.transportMode, group.line);
|
||||
|
||||
// Add line number with transport icon
|
||||
const lineNumber = document.createElement('span');
|
||||
lineNumber.className = 'line-number';
|
||||
|
||||
// Use the first destination as the main one for the header
|
||||
const mainDestination = group.directions[0]?.destination || '';
|
||||
|
||||
lineNumber.innerHTML = `${transportIcon} ${group.lineNumber} <span class="line-destination">${mainDestination}</span>`;
|
||||
header.appendChild(lineNumber);
|
||||
groupCard.appendChild(header);
|
||||
|
||||
// Create the directions container
|
||||
const directionsContainer = document.createElement('div');
|
||||
directionsContainer.className = 'directions-container';
|
||||
|
||||
// Process each direction
|
||||
group.directions.forEach(direction => {
|
||||
// Sort departures by time
|
||||
direction.departures.sort((a, b) => new Date(a.scheduled) - new Date(b.scheduled));
|
||||
|
||||
// Create a row for this direction
|
||||
const directionRow = document.createElement('div');
|
||||
directionRow.className = 'direction-row';
|
||||
|
||||
// Add direction info
|
||||
const directionInfo = document.createElement('div');
|
||||
directionInfo.className = 'direction-info';
|
||||
|
||||
// Determine direction arrow
|
||||
const directionArrow = direction.direction === 1 ? '→' : '←';
|
||||
|
||||
directionInfo.innerHTML = `<span class="direction-arrow">${directionArrow}</span> <span class="direction-destination">${direction.destination}</span>`;
|
||||
directionRow.appendChild(directionInfo);
|
||||
|
||||
// Add times container
|
||||
const timesContainer = document.createElement('div');
|
||||
timesContainer.className = 'times-container';
|
||||
|
||||
// Add up to 2 departure times per direction
|
||||
const maxTimes = 2;
|
||||
direction.departures.slice(0, maxTimes).forEach(departure => {
|
||||
const timeElement = document.createElement('span');
|
||||
timeElement.className = 'time';
|
||||
|
||||
const displayTime = departure.display;
|
||||
const scheduledTime = formatDateTime(departure.scheduled);
|
||||
|
||||
// Check if the display time is just a time (HH:MM) or a countdown
|
||||
const isTimeOnly = /^\d{1,2}:\d{2}$/.test(displayTime);
|
||||
|
||||
// If it's just a time, calculate minutes until arrival
|
||||
let countdownText = displayTime;
|
||||
if (isTimeOnly) {
|
||||
const minutesUntil = calculateMinutesUntilArrival(departure.scheduled);
|
||||
if (minutesUntil <= 0) {
|
||||
countdownText = 'Now';
|
||||
} else if (minutesUntil === 1) {
|
||||
countdownText = '1 min';
|
||||
} else {
|
||||
countdownText = `${minutesUntil} min`;
|
||||
}
|
||||
}
|
||||
|
||||
timeElement.innerHTML = `${scheduledTime} <span class="countdown">(${countdownText})</span>`;
|
||||
timesContainer.appendChild(timeElement);
|
||||
});
|
||||
|
||||
directionRow.appendChild(timesContainer);
|
||||
directionsContainer.appendChild(directionRow);
|
||||
});
|
||||
|
||||
groupCard.appendChild(directionsContainer);
|
||||
|
||||
// Add to container
|
||||
container.appendChild(groupCard);
|
||||
});
|
||||
}
|
||||
|
||||
// Display grouped departures (legacy function)
|
||||
function displayGroupedDepartures(groups, container) {
|
||||
groups.forEach(group => {
|
||||
// Sort departures by time
|
||||
group.departures.sort((a, b) => new Date(a.scheduled) - new Date(b.scheduled));
|
||||
|
||||
// Create a card for this group
|
||||
const groupCard = document.createElement('div');
|
||||
groupCard.className = 'departure-card';
|
||||
|
||||
// Create card header
|
||||
const header = document.createElement('div');
|
||||
header.className = 'departure-header';
|
||||
|
||||
// Get transport icon based on transport mode and line
|
||||
const transportIcon = getTransportIcon(group.line?.transportMode, group.line);
|
||||
|
||||
// Add line number with transport icon and destination
|
||||
const lineNumber = document.createElement('span');
|
||||
lineNumber.className = 'line-number';
|
||||
lineNumber.innerHTML = `${transportIcon} ${group.line.designation} <span class="line-destination">${group.destination}</span>`;
|
||||
header.appendChild(lineNumber);
|
||||
|
||||
// Add times container
|
||||
const timesContainer = document.createElement('div');
|
||||
timesContainer.className = 'times-container';
|
||||
timesContainer.style.display = 'flex';
|
||||
timesContainer.style.flexDirection = 'column';
|
||||
timesContainer.style.alignItems = 'flex-end';
|
||||
|
||||
// Add up to 3 departure times
|
||||
const maxTimes = 3;
|
||||
group.departures.slice(0, maxTimes).forEach((departure, index) => {
|
||||
const timeElement = document.createElement('span');
|
||||
timeElement.className = 'time';
|
||||
timeElement.style.fontSize = index === 0 ? '1.1em' : '0.9em';
|
||||
timeElement.style.marginBottom = '2px';
|
||||
|
||||
const displayTime = departure.display;
|
||||
const scheduledTime = formatDateTime(departure.scheduled);
|
||||
|
||||
// Check if the display time is just a time (HH:MM) or a countdown
|
||||
const isTimeOnly = /^\d{1,2}:\d{2}$/.test(displayTime);
|
||||
|
||||
// If it's just a time, calculate minutes until arrival
|
||||
let countdownText = displayTime;
|
||||
if (isTimeOnly) {
|
||||
const minutesUntil = calculateMinutesUntilArrival(departure.scheduled);
|
||||
if (minutesUntil <= 0) {
|
||||
countdownText = 'Now';
|
||||
} else if (minutesUntil === 1) {
|
||||
countdownText = '1 min';
|
||||
} else {
|
||||
countdownText = `${minutesUntil} min`;
|
||||
}
|
||||
}
|
||||
|
||||
if (isTimeOnly) {
|
||||
timeElement.innerHTML = `${scheduledTime} <span class="countdown">(${countdownText})</span>`;
|
||||
} else {
|
||||
timeElement.innerHTML = `${scheduledTime} <span class="countdown">(${displayTime})</span>`;
|
||||
}
|
||||
|
||||
timesContainer.appendChild(timeElement);
|
||||
});
|
||||
|
||||
// If there are more departures, show a count
|
||||
if (group.departures.length > maxTimes) {
|
||||
const moreElement = document.createElement('span');
|
||||
moreElement.style.fontSize = '0.8em';
|
||||
moreElement.style.color = '#666';
|
||||
moreElement.textContent = `+${group.departures.length - maxTimes} more`;
|
||||
timesContainer.appendChild(moreElement);
|
||||
}
|
||||
|
||||
header.appendChild(timesContainer);
|
||||
groupCard.appendChild(header);
|
||||
|
||||
// No need to add destination and direction separately as they're now in the header
|
||||
|
||||
// Add to container
|
||||
container.appendChild(groupCard);
|
||||
});
|
||||
}
|
||||
|
||||
// Format date and time
|
||||
function formatDateTime(dateTimeString) {
|
||||
const date = new Date(dateTimeString);
|
||||
return date.toLocaleTimeString('sv-SE', { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
// Format relative time (e.g., "in 5 minutes")
|
||||
function formatRelativeTime(dateTimeString) {
|
||||
const departureTime = new Date(dateTimeString);
|
||||
const now = new Date();
|
||||
const diffMinutes = Math.round((departureTime - now) / (1000 * 60));
|
||||
|
||||
if (diffMinutes <= 0) {
|
||||
return 'Now';
|
||||
} else if (diffMinutes === 1) {
|
||||
return 'In 1 minute';
|
||||
} else if (diffMinutes < 60) {
|
||||
return `In ${diffMinutes} minutes`;
|
||||
} else {
|
||||
const hours = Math.floor(diffMinutes / 60);
|
||||
const minutes = diffMinutes % 60;
|
||||
if (minutes === 0) {
|
||||
return `In ${hours} hour${hours > 1 ? 's' : ''}`;
|
||||
} else {
|
||||
return `In ${hours} hour${hours > 1 ? 's' : ''} and ${minutes} minute${minutes > 1 ? 's' : ''}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Group departures by line number
|
||||
function groupDeparturesByLineNumber(departures) {
|
||||
const groups = {};
|
||||
|
||||
departures.forEach(departure => {
|
||||
const lineNumber = departure.line.designation;
|
||||
|
||||
if (!groups[lineNumber]) {
|
||||
groups[lineNumber] = {
|
||||
line: departure.line,
|
||||
directions: {}
|
||||
};
|
||||
}
|
||||
|
||||
const directionKey = `${departure.direction}-${departure.destination}`;
|
||||
|
||||
if (!groups[lineNumber].directions[directionKey]) {
|
||||
groups[lineNumber].directions[directionKey] = {
|
||||
direction: departure.direction,
|
||||
destination: departure.destination,
|
||||
departures: []
|
||||
};
|
||||
}
|
||||
|
||||
groups[lineNumber].directions[directionKey].departures.push(departure);
|
||||
});
|
||||
|
||||
// Convert to array format
|
||||
return Object.entries(groups).map(([lineNumber, data]) => {
|
||||
return {
|
||||
lineNumber: lineNumber,
|
||||
line: data.line,
|
||||
directions: Object.values(data.directions)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Group departures by direction (legacy function kept for compatibility)
|
||||
function groupDeparturesByDirection(departures) {
|
||||
const groups = {};
|
||||
|
||||
departures.forEach(departure => {
|
||||
const key = `${departure.line.designation}-${departure.direction}-${departure.destination}`;
|
||||
|
||||
if (!groups[key]) {
|
||||
groups[key] = {
|
||||
line: departure.line,
|
||||
direction: departure.direction,
|
||||
destination: departure.destination,
|
||||
departures: []
|
||||
};
|
||||
}
|
||||
|
||||
groups[key].departures.push(departure);
|
||||
});
|
||||
|
||||
return Object.values(groups);
|
||||
}
|
||||
|
||||
// Store the current departures data for comparison
|
||||
let currentDepartures = [];
|
||||
|
||||
// Display departures in the UI with smooth transitions
|
||||
function displayDepartures(departures) {
|
||||
if (!departures || departures.length === 0) {
|
||||
departuresContainer.innerHTML = '<div class="error">No departures found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// If this is the first load, just display everything
|
||||
if (currentDepartures.length === 0) {
|
||||
departuresContainer.innerHTML = '';
|
||||
|
||||
departures.forEach(departure => {
|
||||
const departureCard = createDepartureCard(departure);
|
||||
departuresContainer.appendChild(departureCard);
|
||||
});
|
||||
} else {
|
||||
// Update only what has changed
|
||||
updateExistingCards(departures);
|
||||
}
|
||||
|
||||
// Update the current departures for next comparison
|
||||
currentDepartures = JSON.parse(JSON.stringify(departures));
|
||||
}
|
||||
|
||||
// Update existing cards or add new ones
|
||||
function updateExistingCards(newDepartures) {
|
||||
// Get all current cards
|
||||
const currentCards = departuresContainer.querySelectorAll('.departure-card');
|
||||
const currentCardIds = Array.from(currentCards).map(card => card.dataset.journeyId);
|
||||
|
||||
// Process each new departure
|
||||
newDepartures.forEach((departure, index) => {
|
||||
const journeyId = departure.journey.id;
|
||||
const existingCardIndex = currentCardIds.indexOf(journeyId.toString());
|
||||
|
||||
if (existingCardIndex !== -1) {
|
||||
// Update existing card
|
||||
const existingCard = currentCards[existingCardIndex];
|
||||
updateCardContent(existingCard, departure);
|
||||
} else {
|
||||
// This is a new departure, add it
|
||||
const newCard = createDepartureCard(departure);
|
||||
|
||||
// Add with fade-in effect
|
||||
newCard.style.opacity = '0';
|
||||
|
||||
if (index === 0) {
|
||||
// Add to the beginning
|
||||
departuresContainer.prepend(newCard);
|
||||
} else if (index >= departuresContainer.children.length) {
|
||||
// Add to the end
|
||||
departuresContainer.appendChild(newCard);
|
||||
} else {
|
||||
// Insert at specific position
|
||||
departuresContainer.insertBefore(newCard, departuresContainer.children[index]);
|
||||
}
|
||||
|
||||
// Trigger fade-in
|
||||
setTimeout(() => {
|
||||
newCard.style.transition = 'opacity 0.5s ease-in';
|
||||
newCard.style.opacity = '1';
|
||||
}, 10);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove cards that are no longer in the new data
|
||||
const newDepartureIds = newDepartures.map(d => d.journey.id.toString());
|
||||
currentCards.forEach(card => {
|
||||
if (!newDepartureIds.includes(card.dataset.journeyId)) {
|
||||
// Fade out and remove
|
||||
card.style.transition = 'opacity 0.5s ease-out';
|
||||
card.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
if (card.parentNode) {
|
||||
card.parentNode.removeChild(card);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update only the content that has changed in an existing card
|
||||
function updateCardContent(card, departure) {
|
||||
const displayTime = departure.display;
|
||||
const scheduledTime = formatDateTime(departure.scheduled);
|
||||
|
||||
// Check if the display time is just a time (HH:MM) or a countdown
|
||||
const isTimeOnly = /^\d{1,2}:\d{2}$/.test(displayTime);
|
||||
|
||||
// If it's just a time, calculate minutes until arrival
|
||||
let countdownText = displayTime;
|
||||
if (isTimeOnly) {
|
||||
const minutesUntil = calculateMinutesUntilArrival(departure.scheduled);
|
||||
if (minutesUntil <= 0) {
|
||||
countdownText = 'Now';
|
||||
} else if (minutesUntil === 1) {
|
||||
countdownText = '1 min';
|
||||
} else {
|
||||
countdownText = `${minutesUntil} min`;
|
||||
}
|
||||
}
|
||||
|
||||
// Only update the countdown time which changes frequently
|
||||
const countdownElement = card.querySelector('.countdown');
|
||||
|
||||
// Update with subtle highlight effect for changes
|
||||
if (countdownElement && countdownElement.textContent !== `(${countdownText})`) {
|
||||
countdownElement.textContent = `(${countdownText})`;
|
||||
highlightElement(countdownElement);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a subtle highlight effect to show updated content
|
||||
function highlightElement(element) {
|
||||
element.style.transition = 'none';
|
||||
element.style.backgroundColor = 'rgba(255, 255, 0, 0.3)';
|
||||
|
||||
setTimeout(() => {
|
||||
element.style.transition = 'background-color 1.5s ease-out';
|
||||
element.style.backgroundColor = 'transparent';
|
||||
}, 10);
|
||||
}
|
||||
|
||||
// Display multiple sites
|
||||
function displayMultipleSites(sites) {
|
||||
// Get configuration
|
||||
const config = getConfig();
|
||||
const enabledSites = config.sites.filter(site => site.enabled);
|
||||
|
||||
// Clear the container
|
||||
departuresContainer.innerHTML = '';
|
||||
|
||||
// Process each site
|
||||
sites.forEach(site => {
|
||||
// Check if this site is enabled in the configuration
|
||||
const siteConfig = enabledSites.find(s => s.id === site.siteId);
|
||||
if (!siteConfig) return;
|
||||
|
||||
// Create a site container
|
||||
const siteContainer = document.createElement('div');
|
||||
siteContainer.className = 'site-container';
|
||||
|
||||
// Add site header with white tab
|
||||
const siteHeader = document.createElement('div');
|
||||
siteHeader.className = 'site-header';
|
||||
siteHeader.innerHTML = `<span class="site-name">${site.siteName || siteConfig.name}</span>`;
|
||||
siteContainer.appendChild(siteHeader);
|
||||
|
||||
// Process departures for this site
|
||||
if (site.data && site.data.departures) {
|
||||
// Group departures by line number
|
||||
const lineGroups = {};
|
||||
|
||||
site.data.departures.forEach(departure => {
|
||||
const lineNumber = departure.line.designation;
|
||||
if (!lineGroups[lineNumber]) {
|
||||
lineGroups[lineNumber] = [];
|
||||
}
|
||||
lineGroups[lineNumber].push(departure);
|
||||
});
|
||||
|
||||
// Process each line group
|
||||
Object.entries(lineGroups).forEach(([lineNumber, lineDepartures]) => {
|
||||
// Create a line container for side-by-side display
|
||||
const lineContainer = document.createElement('div');
|
||||
lineContainer.className = 'line-container';
|
||||
|
||||
// Group by direction
|
||||
const directionGroups = {};
|
||||
|
||||
lineDepartures.forEach(departure => {
|
||||
const directionKey = `${departure.direction}-${departure.destination}`;
|
||||
if (!directionGroups[directionKey]) {
|
||||
directionGroups[directionKey] = {
|
||||
direction: departure.direction,
|
||||
destination: departure.destination,
|
||||
departures: []
|
||||
};
|
||||
}
|
||||
directionGroups[directionKey].departures.push(departure);
|
||||
});
|
||||
|
||||
// Get all direction groups
|
||||
const directions = Object.values(directionGroups);
|
||||
|
||||
// Handle single direction case (like bus 4)
|
||||
if (directions.length === 1) {
|
||||
// Create a full-width card for this direction
|
||||
const directionGroup = directions[0];
|
||||
|
||||
// Sort departures by time
|
||||
directionGroup.departures.sort((a, b) => new Date(a.scheduled) - new Date(b.scheduled));
|
||||
|
||||
// Create a card for this direction
|
||||
const directionCard = document.createElement('div');
|
||||
directionCard.className = 'departure-card';
|
||||
// Don't set width to 100% as it causes the card to stick out
|
||||
|
||||
// Create a simplified layout with line number and times on the same row
|
||||
const cardContent = document.createElement('div');
|
||||
cardContent.className = 'departure-header';
|
||||
cardContent.style.display = 'flex';
|
||||
cardContent.style.justifyContent = 'space-between';
|
||||
cardContent.style.alignItems = 'center';
|
||||
|
||||
// Get transport icon based on transport mode and line
|
||||
const transportIcon = getTransportIcon(directionGroup.departures[0].line?.transportMode, directionGroup.departures[0].line);
|
||||
|
||||
// Add line number with transport icon and destination
|
||||
const lineNumberElement = document.createElement('span');
|
||||
lineNumberElement.className = 'line-number';
|
||||
lineNumberElement.innerHTML = `${transportIcon} ${lineNumber} <span class="line-destination">${directionGroup.destination}</span>`;
|
||||
|
||||
// Add times container
|
||||
const timesContainer = document.createElement('div');
|
||||
timesContainer.className = 'times-container';
|
||||
timesContainer.style.display = 'flex';
|
||||
timesContainer.style.flexDirection = 'column';
|
||||
timesContainer.style.alignItems = 'flex-end';
|
||||
|
||||
// Add up to 2 departure times
|
||||
const maxTimes = 2;
|
||||
directionGroup.departures.slice(0, maxTimes).forEach(departure => {
|
||||
const timeElement = document.createElement('div');
|
||||
timeElement.className = 'time';
|
||||
timeElement.style.fontSize = '1.1em';
|
||||
timeElement.style.marginBottom = '2px';
|
||||
timeElement.style.whiteSpace = 'nowrap';
|
||||
timeElement.style.textAlign = 'right';
|
||||
|
||||
const displayTime = departure.display;
|
||||
const scheduledTime = formatDateTime(departure.scheduled);
|
||||
|
||||
// Check if the display time is just a time (HH:MM) or a countdown
|
||||
const isTimeOnly = /^\d{1,2}:\d{2}$/.test(displayTime);
|
||||
|
||||
// If it's just a time, calculate minutes until arrival
|
||||
let countdownText = displayTime;
|
||||
if (isTimeOnly) {
|
||||
const minutesUntil = calculateMinutesUntilArrival(departure.scheduled);
|
||||
if (minutesUntil <= 0) {
|
||||
countdownText = 'Now';
|
||||
} else if (minutesUntil === 1) {
|
||||
countdownText = '1 min';
|
||||
} else {
|
||||
countdownText = `${minutesUntil} min`;
|
||||
}
|
||||
}
|
||||
|
||||
timeElement.textContent = `${scheduledTime} (${countdownText})`;
|
||||
timeElement.style.width = '140px'; // Fixed width to prevent overflow
|
||||
timeElement.style.width = '140px'; // Fixed width to prevent overflow
|
||||
|
||||
timesContainer.appendChild(timeElement);
|
||||
});
|
||||
|
||||
cardContent.appendChild(lineNumberElement);
|
||||
cardContent.appendChild(timesContainer);
|
||||
directionCard.appendChild(cardContent);
|
||||
siteContainer.appendChild(directionCard);
|
||||
} else {
|
||||
// Create cards for each direction, with max 2 per row
|
||||
// Create a new line container for every 2 directions
|
||||
for (let i = 0; i < directions.length; i += 2) {
|
||||
// Create a new line container for this pair of directions
|
||||
const rowContainer = document.createElement('div');
|
||||
rowContainer.className = 'line-container';
|
||||
|
||||
// Process up to 2 directions for this row
|
||||
for (let j = i; j < i + 2 && j < directions.length; j++) {
|
||||
const directionGroup = directions[j];
|
||||
|
||||
// Sort departures by time
|
||||
directionGroup.departures.sort((a, b) => new Date(a.scheduled) - new Date(b.scheduled));
|
||||
|
||||
// Create a card for this direction
|
||||
const directionCard = document.createElement('div');
|
||||
directionCard.className = 'departure-card direction-card';
|
||||
|
||||
// Create a simplified layout with line number and times on the same row
|
||||
const cardContent = document.createElement('div');
|
||||
cardContent.className = 'departure-header';
|
||||
cardContent.style.display = 'flex';
|
||||
cardContent.style.justifyContent = 'space-between';
|
||||
cardContent.style.alignItems = 'center';
|
||||
|
||||
// Get transport icon based on transport mode and line
|
||||
const transportIcon = getTransportIcon(directionGroup.departures[0].line?.transportMode, directionGroup.departures[0].line);
|
||||
|
||||
// Add line number with transport icon and destination
|
||||
const lineNumberElement = document.createElement('span');
|
||||
lineNumberElement.className = 'line-number';
|
||||
lineNumberElement.innerHTML = `${transportIcon} ${lineNumber} <span class="line-destination">${directionGroup.destination}</span>`;
|
||||
|
||||
// Add times container
|
||||
const timesContainer = document.createElement('div');
|
||||
timesContainer.className = 'times-container';
|
||||
timesContainer.style.display = 'flex';
|
||||
timesContainer.style.flexDirection = 'column';
|
||||
timesContainer.style.alignItems = 'flex-end';
|
||||
|
||||
// Add up to 2 departure times
|
||||
const maxTimes = 2;
|
||||
directionGroup.departures.slice(0, maxTimes).forEach(departure => {
|
||||
const timeElement = document.createElement('div');
|
||||
timeElement.className = 'time';
|
||||
timeElement.style.fontSize = '1.1em';
|
||||
timeElement.style.marginBottom = '2px';
|
||||
timeElement.style.whiteSpace = 'nowrap';
|
||||
timeElement.style.textAlign = 'right';
|
||||
|
||||
const displayTime = departure.display;
|
||||
const scheduledTime = formatDateTime(departure.scheduled);
|
||||
|
||||
// Check if the display time is just a time (HH:MM) or a countdown
|
||||
const isTimeOnly = /^\d{1,2}:\d{2}$/.test(displayTime);
|
||||
|
||||
// If it's just a time, calculate minutes until arrival
|
||||
let countdownText = displayTime;
|
||||
if (isTimeOnly) {
|
||||
const minutesUntil = calculateMinutesUntilArrival(departure.scheduled);
|
||||
if (minutesUntil <= 0) {
|
||||
countdownText = 'Now';
|
||||
} else if (minutesUntil === 1) {
|
||||
countdownText = '1 min';
|
||||
} else {
|
||||
countdownText = `${minutesUntil} min`;
|
||||
}
|
||||
}
|
||||
|
||||
timeElement.textContent = `${scheduledTime} (${countdownText})`;
|
||||
|
||||
timesContainer.appendChild(timeElement);
|
||||
});
|
||||
|
||||
cardContent.appendChild(lineNumberElement);
|
||||
cardContent.appendChild(timesContainer);
|
||||
directionCard.appendChild(cardContent);
|
||||
rowContainer.appendChild(directionCard);
|
||||
}
|
||||
|
||||
// Add this row to the site container
|
||||
siteContainer.appendChild(rowContainer);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (site.error) {
|
||||
// Display error for this site
|
||||
const errorElement = document.createElement('div');
|
||||
errorElement.className = 'error';
|
||||
errorElement.textContent = `Error loading departures for ${site.siteName}: ${site.error}`;
|
||||
siteContainer.appendChild(errorElement);
|
||||
}
|
||||
|
||||
// Add the site container to the main container
|
||||
departuresContainer.appendChild(siteContainer);
|
||||
});
|
||||
}
|
||||
|
||||
// Get configuration
|
||||
function getConfig() {
|
||||
// Default configuration
|
||||
const defaultConfig = {
|
||||
combineSameDirection: true,
|
||||
sites: [
|
||||
{
|
||||
id: '1411',
|
||||
name: 'Ambassaderna',
|
||||
enabled: true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// If we have a ConfigManager instance, use its config
|
||||
if (window.configManager && window.configManager.config) {
|
||||
return {
|
||||
combineSameDirection: window.configManager.config.combineSameDirection !== undefined ?
|
||||
window.configManager.config.combineSameDirection : defaultConfig.combineSameDirection,
|
||||
sites: window.configManager.config.sites || defaultConfig.sites
|
||||
};
|
||||
}
|
||||
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
// Fetch departures from our proxy server
|
||||
async function fetchDepartures() {
|
||||
try {
|
||||
// Don't show loading status to avoid layout disruptions
|
||||
// statusElement.textContent = 'Loading departures...';
|
||||
|
||||
const response = await fetch(API_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.sites && Array.isArray(data.sites)) {
|
||||
// Process multiple sites
|
||||
displayMultipleSites(data.sites);
|
||||
|
||||
const now = new Date();
|
||||
lastUpdatedElement.textContent = `Last updated: ${now.toLocaleTimeString('sv-SE')}`;
|
||||
} else if (data.departures) {
|
||||
// Legacy format - single site
|
||||
displayDepartures(data.departures);
|
||||
|
||||
const now = new Date();
|
||||
lastUpdatedElement.textContent = `Last updated: ${now.toLocaleTimeString('sv-SE')}`;
|
||||
} else if (data.error) {
|
||||
throw new Error(data.error);
|
||||
} else {
|
||||
throw new Error('Invalid response format from server');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching departures:', error);
|
||||
// Don't update status element to avoid layout disruptions
|
||||
// statusElement.textContent = '';
|
||||
departuresContainer.innerHTML = `
|
||||
<div class="error">
|
||||
<p>Failed to load departures. Please try again later.</p>
|
||||
<p>Error: ${error.message}</p>
|
||||
<p>Make sure the Node.js server is running: <code>node server.js</code></p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Set up auto-refresh
|
||||
function setupAutoRefresh() {
|
||||
// Clear any existing timer
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
}
|
||||
|
||||
// Set up new timer
|
||||
refreshTimer = setInterval(fetchDepartures, REFRESH_INTERVAL);
|
||||
}
|
||||
|
||||
// Initialize departures functionality
|
||||
function initDepartures() {
|
||||
// API endpoint (using our local proxy server)
|
||||
window.API_URL = 'http://localhost:3002/api/departures';
|
||||
|
||||
// DOM elements
|
||||
window.departuresContainer = document.getElementById('departures');
|
||||
window.statusElement = document.getElementById('status');
|
||||
window.lastUpdatedElement = document.getElementById('last-updated');
|
||||
|
||||
// Auto-refresh interval (in milliseconds) - 5 seconds
|
||||
window.REFRESH_INTERVAL = 5000;
|
||||
window.refreshTimer = null;
|
||||
|
||||
// Initial fetch and setup
|
||||
fetchDepartures();
|
||||
setupAutoRefresh();
|
||||
}
|
||||
|
||||
// Initialize when the DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initDepartures();
|
||||
});
|
||||
Reference in New Issue
Block a user