Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| 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(); | |