import { MapView } from './components/Map.js'; import { Timeline } from './components/Timeline.js'; import { EventCard } from './components/EventCard.js'; import { Countdown } from './components/Countdown.js'; import { HOME_BASE } from './config.js'; import airshowData from './data/airshows.json'; const FLIGHT_ANIMATION_DURATION = 2000; // ms const MOBILE_BREAKPOINT = 768; const isMobile = () => window.innerWidth <= MOBILE_BREAKPOINT; class App { constructor() { this.map = null; this.timeline = null; this.eventCard = null; this.countdown = null; this.isPlaying = false; this.init(); } init() { // Initialize event card this.eventCard = new EventCard(document.getElementById('event-card')); this.eventCard.setOnClose(() => this.handleCardClose()); // Initialize countdown this.countdown = new Countdown(document.getElementById('countdown')); this.initCountdown(); // Initialize map this.map = new MapView('map', { onEventSelect: (event) => this.handleMapEventClick(event) }); // Initialize timeline this.timeline = new Timeline(document.getElementById('timeline')); this.timeline.setOnEventSelect((event, index) => this.handleTimelineSelect(event, index)); this.timeline.setOnPlayStateChange((playing) => this.handlePlayStateChange(playing)); this.timeline.setEvents(airshowData.events); // Set events on map after it loads this.map.getMap().on('load', () => { this.map.setEvents(airshowData.events); }); // Keyboard navigation document.addEventListener('keydown', (e) => this.handleKeydown(e)); } initCountdown() { // Find next upcoming event const now = new Date(); const sortedEvents = [...airshowData.events].sort((a, b) => new Date(a.date) - new Date(b.date)); const nextEvent = sortedEvents.find(e => new Date(e.date) >= now); if (nextEvent) { this.countdown.setNextEvent(nextEvent); } } handleMapEventClick(event) { // Clicking map marker pauses playback and selects event if (this.isPlaying) { this.timeline.pause(); this.isPlaying = false; } this.timeline.selectEvent(event.id); } handleTimelineSelect(event, index) { // Update map marker selection this.map.selectEvent(event.id); // Show event card this.eventCard.show(event); // Get the "from" location (previous event or home base) const prevEvent = index > 0 ? this.timeline.sortedEvents[index - 1] : null; const fromCoords = prevEvent ? prevEvent.location.coordinates : HOME_BASE.coordinates; const toCoords = event.location.coordinates; if (this.isPlaying) { // Animate the flight path being traced (no zoom/pan during playback) this.map.animateFlightPath(fromCoords, toCoords, FLIGHT_ANIMATION_DURATION, () => { // When animation completes, advance to next event if (this.isPlaying) { this.advanceToNextEvent(); } }); } else { // Show static full path to next event this.map.flyTo(toCoords, 8); const nextEvent = this.timeline.getNextEvent(); if (nextEvent) { this.map.showFlightPath(toCoords, nextEvent.location.coordinates); } else { this.map.hideFlightPath(); } } } handlePlayStateChange(playing) { this.isPlaying = playing; if (playing) { // On mobile, zoom out to show all of France during playback if (isMobile()) { this.map.zoomToFrance(); } // Start playback if (this.timeline.selectedIndex < 0) { // No event selected, start from first this.timeline.selectEventByIndex(0); } else { // Re-trigger current selection to start animation const currentEvent = this.timeline.getCurrentEvent(); if (currentEvent) { this.handleTimelineSelect(currentEvent, this.timeline.selectedIndex); } } } else { // Paused - show static path to next event const currentEvent = this.timeline.getCurrentEvent(); const nextEvent = this.timeline.getNextEvent(); if (currentEvent && nextEvent) { this.map.showFlightPath(currentEvent.location.coordinates, nextEvent.location.coordinates); } } } advanceToNextEvent() { const hasNext = this.timeline.nextEvent(); if (!hasNext) { // Reached end, stop playing this.timeline.pause(); this.isPlaying = false; this.map.hideFlightPath(); } } handleCardClose() { // Pause if playing if (this.isPlaying) { this.timeline.pause(); this.isPlaying = false; } this.timeline.clearSelection(); this.map.selectEvent(null); this.map.resetView(); } handleKeydown(e) { if (e.key === 'Escape') { this.handleCardClose(); } else if (e.key === ' ' || e.code === 'Space') { e.preventDefault(); this.timeline.togglePlay(); } else if (e.key === 'ArrowRight' && !this.isPlaying) { this.timeline.nextEvent(); } } } // Start app new App();