// 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 = `
${stats.personnel}
Personnel
${stats.sorties}
Sorties
${stats.medals}
Medals
${stats.duration}
Months
`; } renderNavigationCards() { const cardsContainer = document.getElementById('navigationCards'); if (!cardsContainer) return; const cardsHTML = this.data.sections.map(section => ` `).join(''); cardsContainer.innerHTML = cardsHTML; // Add click handlers cardsContainer.querySelectorAll('.nav-card').forEach(card => { card.addEventListener('click', () => { const sectionId = card.dataset.section; this.showSection(sectionId); }); }); } async showSection(sectionId) { try { const section = this.data.sections.find(s => s.id === sectionId); if (!section) { throw new Error(`Section ${sectionId} not found`); } // Hide main navigation const mainNav = document.getElementById('mainNavigation'); if (mainNav) mainNav.classList.add('hidden'); // Show section container and back button const sectionContainer = document.getElementById('sectionContainer'); const backButton = document.getElementById('backButton'); if (sectionContainer) sectionContainer.classList.remove('hidden'); if (backButton) backButton.classList.add('show'); // Load section content await this.loadSectionContent(section); // Update map for section this.updateMapForSection(section); this.currentSection = sectionId; } catch (error) { console.error(`Error loading section ${sectionId}:`, error); this.showErrorMessage(`Failed to load ${sectionId} section. Please try again.`); } } async loadSectionContent(section) { const sectionContainer = document.getElementById('sectionContainer'); if (!sectionContainer) return; // 1. Check localStorage for builder-saved blocks const saved = localStorage.getItem('pf-section-' + section.id); if (saved) { try { const blocks = JSON.parse(saved); if (typeof BlockRenderer !== 'undefined') { sectionContainer.innerHTML = this.wrapSectionContent(section, BlockRenderer.render(blocks)); this.initializeSectionInteractions(section.id); return; } } catch (e) { /* fall through */ } } // 2. Use sections-content.json block data const contentData = this.data.sectionsContent && this.data.sectionsContent[section.id]; if (contentData && contentData.blocks && typeof BlockRenderer !== 'undefined') { sectionContainer.innerHTML = this.wrapSectionContent(section, BlockRenderer.render(contentData.blocks)); this.initializeSectionInteractions(section.id); return; } // 3. Try legacy HTML file try { const response = await fetch('sections/' + section.id + '.html'); if (!response.ok) throw new Error('No legacy HTML'); sectionContainer.innerHTML = await response.text(); this.initializeSectionInteractions(section.id); } catch (error) { // 4. Fallback placeholder this.loadFallbackSectionContent(section); } } wrapSectionContent(section, blocksHtml) { return '
' + '
' + '
' + '' + section.icon + '' + '

' + section.title + '

' + '

' + section.subtitle + '

' + '
' + blocksHtml + '
'; } loadFallbackSectionContent(section) { const sectionContainer = document.getElementById('sectionContainer'); if (!sectionContainer) return; sectionContainer.innerHTML = `
${section.icon} ${section.title}
${section.subtitle}

Content Loading

This section is being developed. Please check back later for complete content.

`; } initializeSectionInteractions(sectionId) { if (typeof BlockRenderer !== 'undefined' && typeof BlockRenderer.initGalleries === 'function') { BlockRenderer.initGalleries(document.getElementById('sectionContainer')); } // Wire map-button blocks to the Leaflet map const self = this; document.querySelectorAll('.block-map-btn').forEach(function(btn) { btn.addEventListener('click', function() { const lat = parseFloat(btn.dataset.lat); const lng = parseFloat(btn.dataset.lng); const zoom = parseInt(btn.dataset.zoom) || 6; if (self.map) { self.map.setView([lat, lng], zoom); btn.classList.add('active'); setTimeout(function() { btn.classList.remove('active'); }, 1500); } }); }); } updateMapForSection(section) { if (!this.map) return; // Clear existing layers Object.values(this.mapLayers).forEach(layer => { this.map.removeLayer(layer); layer.clearLayers(); }); // Add section-specific map data const sectionMapData = this.data.mapData[section.id]; if (sectionMapData) { this.loadMapData(sectionMapData); } // Update map view for section this.setMapViewForSection(section.id); } setMapViewForSection(sectionId) { if (!this.map) return; const mapViews = { context: { center: [29.3759, 47.9774], zoom: 6 }, rcaf: { center: [25.2731, 51.6080], zoom: 8 }, rcn: { center: [27.0, 50.0], zoom: 7 }, army: { center: [32.0, 45.0], zoom: 6 }, legacy: { center: [35.0, 45.0], zoom: 5 } }; const view = mapViews[sectionId] || mapViews.context; this.map.setView(view.center, view.zoom); } loadMapData(mapData) { if (!this.map || !window.L) return; // Load markers, routes, and other geographic data if (mapData.markers) { mapData.markers.forEach(marker => { const mapMarker = L.marker([marker.lat, marker.lng]) .bindPopup(`${marker.title}
${marker.description || ''}`); const layerGroup = this.mapLayers[marker.type] || this.mapLayers.events; layerGroup.addLayer(mapMarker); }); } // Add all layers to map Object.values(this.mapLayers).forEach(layer => { this.map.addLayer(layer); }); } resetMapView() { if (!this.map) return; // Reset to overview this.map.setView([29.3759, 47.9774], 6); // Clear section-specific layers Object.values(this.mapLayers).forEach(layer => { this.map.removeLayer(layer); layer.clearLayers(); }); // Reload overview markers this.loadOverviewMarkers(); } toggleSidebar() { const sidebar = document.getElementById('sidebar'); const toggleIcon = document.getElementById('toggle-icon'); if (sidebar && toggleIcon) { sidebar.classList.toggle('collapsed'); toggleIcon.textContent = sidebar.classList.contains('collapsed') ? 'β—€' : 'β–Ά'; } } showMainNavigation() { this.loadMainNavigation(); } handleResize() { // Handle responsive behavior if (this.map) { this.map.invalidateSize(); } } handleKeyboard(event) { // Handle keyboard navigation if (event.key === 'Escape') { if (this.currentSection) { this.showMainNavigation(); } } } showLoadingScreen() { const loadingScreen = document.getElementById('loadingScreen'); if (loadingScreen) { loadingScreen.style.display = 'flex'; } } hideLoadingScreen() { const loadingScreen = document.getElementById('loadingScreen'); if (loadingScreen) { setTimeout(() => { loadingScreen.classList.add('fade-out'); setTimeout(() => { loadingScreen.style.display = 'none'; }, 500); }, 1000); } } showErrorMessage(message) { console.error(message); // In production, this would show a user-friendly error dialog alert(message); } } // Initialize the application when DOM is ready document.addEventListener('DOMContentLoaded', () => { window.projectFriction = new ProjectFriction(); }); // Export for module usage if (typeof module !== 'undefined' && module.exports) { module.exports = ProjectFriction; }