' + section.title + '
' + '' + section.subtitle + '
// Project Friction - Main Application // Museum-quality digital exhibit for Canada's Gulf War role class ProjectFriction { constructor() { this.currentSection = null; this.map = null; this.mapLayers = {}; this.timeline = null; this.data = { sections: [], mapData: {}, timelineEvents: [], articles: {} }; this.init(); } async init() { try { // Show loading screen this.showLoadingScreen(); // Load data await this.loadApplicationData(); // Initialize components this.initializeMap(); this.initializeNavigation(); this.initializeTimeline(); this.initializeInteractions(); // Setup event listeners this.setupEventListeners(); // Load initial state this.loadMainNavigation(); // Hide loading screen this.hideLoadingScreen(); console.log('Project Friction initialized successfully'); } catch (error) { console.error('Error initializing Project Friction:', error); this.showErrorMessage('Failed to load exhibition. Please refresh the page.'); } } async loadApplicationData() { // Load optional datasets without failing app startup if files are missing. this.data.sections = await this.loadOptionalJSON(['data/sections.json'], []); this.data.mapData = await this.loadOptionalJSON(['data/map-data.json', 'map-data.json'], {}); this.data.timelineEvents = await this.loadOptionalJSON(['data/timeline-events.json'], []); this.data.articles = await this.loadOptionalJSON(['data/articles.json'], {}); // Load block-based section content (used by the block renderer) this.data.sectionsContent = await this.loadOptionalJSON(['data/sections-content.json'], {}); if (!Array.isArray(this.data.sections) || this.data.sections.length === 0) { this.loadDefaultConfiguration(); } this.mergeBuilderSectionsFromStorage(); } mergeBuilderSectionsFromStorage() { if (typeof localStorage === 'undefined') return; var raw = localStorage.getItem('pf-builder-sections'); if (!raw) return; try { var parsed = JSON.parse(raw); var order = Array.isArray(parsed.order) ? parsed.order : Object.keys(parsed.sections || {}); var sectionMeta = parsed.sections || {}; if (!Array.isArray(this.data.sections)) this.data.sections = []; var existingById = {}; this.data.sections.forEach(function(section) { existingById[section.id] = section; }); order.forEach(function(id) { var meta = sectionMeta[id]; if (!meta) return; var stats = { articles: parseInt(meta.stats && meta.stats.articles, 10) || 0, storyMaps: parseInt(meta.stats && meta.stats.storyMaps, 10) || 0, images: parseInt(meta.stats && meta.stats.images, 10) || 0 }; if (existingById[id]) { existingById[id].title = meta.title || existingById[id].title; existingById[id].subtitle = meta.subtitle || existingById[id].subtitle; existingById[id].description = meta.description || existingById[id].description; existingById[id].icon = meta.icon || existingById[id].icon; existingById[id].color = meta.color || existingById[id].color; existingById[id].stats = stats; } else { var newSection = { id: id, title: meta.title || id, subtitle: meta.subtitle || '', icon: meta.icon || 'π', color: meta.color || '#7f8c8d', description: meta.description || '', stats: stats }; this.data.sections.push(newSection); existingById[id] = newSection; } }, this); // Keep display order aligned with builder tabs for kiosk consistency. var reordered = []; order.forEach(function(id) { if (existingById[id]) reordered.push(existingById[id]); }); this.data.sections.forEach(function(section) { if (order.indexOf(section.id) === -1) reordered.push(section); }); this.data.sections = reordered; } catch (error) { console.warn('Could not parse builder section metadata from localStorage.'); } } async loadJSON(url) { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to load ${url}: ${response.status}`); } return await response.json(); } async loadOptionalJSON(paths, fallbackValue) { for (const path of paths) { try { return await this.loadJSON(path); } catch (error) { console.warn(`Optional data file not available: ${path}`); } } return fallbackValue; } loadDefaultConfiguration() { // Fallback configuration if data files fail to load this.data.sections = [ { id: 'context', title: 'Historical Context', subtitle: 'Coalition Building & Diplomacy', icon: 'π', color: '#d4a574', description: 'Understand how Iraq\'s invasion of Kuwait triggered the largest military coalition since WWII and Canada\'s diplomatic role.', stats: { articles: 7, storyMaps: 3, images: 45 } }, { id: 'rcaf', title: 'RCAF Desert Cats', subtitle: '409 Squadron Air Operations', icon: 'βοΈ', color: '#3498db', description: 'Follow the Desert Cats from defensive patrols to strike missions, including the TNC-45 engagement and bombing runs.', stats: { articles: 8, storyMaps: 4, images: 60 } }, { id: 'rcn', title: 'RCN Task Group', subtitle: 'Naval Blockade Operations', icon: 'β', color: '#2c3e50', description: 'The transformation of HMCS Terra Nova into a guided-missile destroyer and enforcement of UN sanctions.', stats: { articles: 7, storyMaps: 2, images: 35 } }, { id: 'army', title: 'Canadian Army', subtitle: 'Medical & Security Forces', icon: 'π₯', color: '#27ae60', description: '1 Canadian Field Hospital\'s life-saving work and security operations protecting coalition installations.', stats: { articles: 7, storyMaps: 3, images: 40 } }, { id: 'legacy', title: 'After the War', subtitle: 'Veterans & Long-term Impact', icon: 'π', color: '#c0392b', description: 'Gulf War Syndrome, environmental cleanup, peacekeeping missions, and the ongoing support for veterans.', stats: { articles: 7, storyMaps: 2, images: 30 } } ]; // Default stats for main navigation this.data.globalStats = { personnel: '4,500+', sorties: '2,700', medals: '4,449', duration: '7' }; } initializeMap() { if (!window.L) { console.warn('Leaflet is unavailable. Map features are disabled.'); return; } const mapElement = document.getElementById('map'); if (!mapElement) { console.warn('Map container not found.'); return; } // Initialize Leaflet map this.map = L.map('map', { center: [29.3759, 47.9774], // Kuwait City zoom: 6, zoomControl: false }); // Add tile layer L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: 'Β© OpenStreetMap contributors', maxZoom: 18 }).addTo(this.map); // Initialize layer groups this.mapLayers = { coalition: L.layerGroup(), iraqi: L.layerGroup(), canadian: L.layerGroup(), events: L.layerGroup(), routes: L.layerGroup() }; // Add initial overview markers this.loadOverviewMarkers(); } loadOverviewMarkers() { if (!this.map || !window.L) return; // Add basic overview markers const overviewMarkers = [ { lat: 29.3759, lng: 47.9774, title: 'Kuwait City', type: 'liberation' }, { lat: 25.2731, lng: 51.6080, title: 'Doha Air Base', type: 'canadian' }, { lat: 26.2285, lng: 50.5860, title: 'Naval Operations', type: 'canadian' } ]; overviewMarkers.forEach(marker => { const mapMarker = L.marker([marker.lat, marker.lng]) .bindPopup(`${marker.title}`) .addTo(this.map); }); } initializeNavigation() { if (typeof NavigationController === 'undefined') { console.warn('NavigationController is unavailable. Using built-in navigation only.'); return; } this.navigation = new NavigationController(this); this.navigation.init(); } initializeTimeline() { if (typeof TimelineController === 'undefined') { console.warn('TimelineController is unavailable.'); return; } this.timeline = new TimelineController(this); this.timeline.init(); } initializeInteractions() { if (typeof InteractionController === 'undefined') { console.warn('InteractionController is unavailable.'); return; } this.interactions = new InteractionController(this); this.interactions.init(); } setupEventListeners() { // Sidebar toggle const sidebarToggle = document.getElementById('sidebarToggle'); if (sidebarToggle) { sidebarToggle.addEventListener('click', () => this.toggleSidebar()); } // Back button const backButton = document.getElementById('backButton'); if (backButton) { backButton.addEventListener('click', () => this.showMainNavigation()); } // Window resize handler window.addEventListener('resize', () => this.handleResize()); // Keyboard navigation document.addEventListener('keydown', (e) => this.handleKeyboard(e)); } loadMainNavigation() { // Show main navigation const mainNav = document.getElementById('mainNavigation'); const sectionContainer = document.getElementById('sectionContainer'); const backButton = document.getElementById('backButton'); if (mainNav) mainNav.classList.remove('hidden'); if (sectionContainer) sectionContainer.classList.add('hidden'); if (backButton) backButton.classList.remove('show'); // Load navigation cards and stats this.renderQuickStats(); this.renderNavigationCards(); // Reset map to overview this.resetMapView(); this.currentSection = null; } renderQuickStats() { const statsContainer = document.getElementById('quickStats'); if (!statsContainer) return; const stats = this.data.globalStats || { personnel: '4,500+', sorties: '2,700', medals: '4,449', duration: '7' }; statsContainer.innerHTML = `
' + section.subtitle + '
This section is being developed. Please check back later for complete content.