paf-tracker / src /main.js
hlarcher's picture
hlarcher HF Staff
Mobile optimizations and Safran logo
878012f unverified
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();