/** * Main Application Entry Point * * Demonstrates integration of: * - Bootstrap 5.3 for UI components * - SQLocal (SQLite in browser via OPFS) * - BroadcastChannel for cross-tab sync * - OpenLayers map with ol-ext LayerSwitcher * - PWA features (Service Worker, install prompt, offline detection) */ // Bootstrap CSS and JS import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap-icons/font/bootstrap-icons.css'; import { Modal, Offcanvas } from 'bootstrap'; // Database module (uses SQLocal directly, BroadcastChannel for tab sync) import { sql, dbReady, initSchema, addLocation, getLocations, getLocationCount, getDatabaseStatus, downloadDatabase, onDatabaseChange, exportToGeoJSON } from './src/database.js'; // Map component with OpenLayers and ol-ext LayerSwitcher import { MapView } from './src/components/MapView.js'; // PWA module (registers Service Worker, handles install/offline) import { initPWA, isOnline, onOfflineChange } from './src/pwa.js'; // Map instance (global for access across functions) let mapView = null; // ============================================================================ // Application Initialization // ============================================================================ async function initApp() { console.log('[App] Initializing...'); // 1. Initialize PWA features (Service Worker, install prompt, offline detection) await initPWA({ installButton: '#install-btn', offlineIndicator: '#offline-indicator', autoRegisterSW: true }); // 2. Initialize the map mapView = new MapView('map', { center: [-1.5, 7.5], // Ghana zoom: 7, basemap: 'osm' }); // Category emojis are set up in MapView: // 'water': '💧', 'school': '🏫', 'health': '🏥', // 'market': '🏪', 'default': '📍', 'other': '📌' // Set up map click handler immediately after map creation mapView.onClick((lon, lat, feature, evt) => { console.log('[App] Map clicked:', lon, lat, feature); if (feature) { // Clicked on existing marker - select it and show details mapView.selectMarker(feature); showLocationDetails(feature); } else { // Clicked on empty space - show add location popup at click position mapView.clearSelection(); mapView.showAddLocationPopup(evt.coordinate); } }); // Set up handler for the map add location popup form mapView.onAddLocation(async (data) => { console.log('[App] Add location from map popup:', data); try { const result = await addLocation(data.name, data.lon, data.lat, { description: data.description || null, category: data.category || 'default' }); console.log('[App] Location added:', data.name, 'id:', result.id); await loadLocations(); // Zoom to the new location on the map mapView?.zoomTo(data.lon, data.lat, 14); // Select the new marker if (result.id) { mapView?.selectMarker(result.id); } showSuccess('Location added successfully'); } catch (error) { console.error('[App] Failed to add location:', error); showError('Failed to add location: ' + error.message); } }); // 3. Initialize database try { console.log('[App] Initializing database...'); // Initialize schema (creates tables if they don't exist) // This also resolves dbReady when complete await initSchema(); // Now dbReady should be resolved console.log('[App] Database ready'); // Show database status const status = await getDatabaseStatus(); console.log('[App] Database status:', status); } catch (error) { console.error('[App] Database initialization failed:', error); showError('Failed to initialize database. Please refresh the page.'); return; } // 4. Initialize UI initUI(); // 5. Load initial data and display on map await loadLocations(); // 6. Listen for database changes from other tabs onDatabaseChange((change) => { console.log('[App] Database change:', change); if (change.table === 'locations' && !change.local) { // Reload locations when another tab makes changes loadLocations(); } }); // 7. Set up offline handling onOfflineChange((offline) => { if (offline) { console.log('[App] Working offline - data will sync when back online'); } else { console.log('[App] Back online - syncing data...'); syncData(); } }); console.log('[App] Initialized successfully'); } // ============================================================================ // UI Initialization // ============================================================================ function initUI() { // Export button const exportBtn = document.getElementById('export-btn'); if (exportBtn) { exportBtn.addEventListener('click', handleExport); } // GeoJSON Export button const exportGeoJSONBtn = document.getElementById('exportGeoJSON-btn'); if (exportGeoJSONBtn) { exportGeoJSONBtn.addEventListener('click', handleExportGeoJSON); } // Status button const statusBtn = document.getElementById('status-btn'); if (statusBtn) { statusBtn.addEventListener('click', handleShowStatus); } // Fit to markers button const fitBtn = document.getElementById('fit-btn'); if (fitBtn) { fitBtn.addEventListener('click', () => mapView?.fitToMarkers()); } } // ============================================================================ // Location Handlers // ============================================================================ async function handleAddLocation(event) { event.preventDefault(); const form = event.target; const formData = new FormData(form); const name = formData.get('name'); const longitude = parseFloat(formData.get('longitude')); const latitude = parseFloat(formData.get('latitude')); const description = formData.get('description') || null; const category = formData.get('category') || 'default'; if (!name || isNaN(longitude) || isNaN(latitude)) { showError('Please fill in all required fields'); return; } try { const result = await addLocation(name, longitude, latitude, { description, category }); console.log('[App] Location added:', name, 'id:', result.id); form.reset(); await loadLocations(); // Zoom to the new location on the map mapView?.zoomTo(longitude, latitude, 14); // Select the new marker if (result.id) { mapView?.selectMarker(result.id); } showSuccess('Location added successfully'); } catch (error) { console.error('[App] Failed to add location:', error); showError('Failed to add location: ' + error.message); } } async function loadLocations() { try { console.log('[App] Loading locations...'); const locations = await getLocations(); console.log('[App] Locations loaded:', locations); // Update the list renderLocations(locations); // Update the map markers if (mapView) { mapView.clearMarkers(); if (locations.length > 0) { mapView.addMarkers(locations); console.log('[App] Added', locations.length, 'markers to map'); } } // Update count display const countEl = document.getElementById('location-count'); if (countEl) { countEl.textContent = locations.length; } } catch (error) { console.error('[App] Failed to load locations:', error); } } /** * Show details for a selected location */ function showLocationDetails(feature) { const name = feature.get('name'); const description = feature.get('description'); const category = feature.get('category'); const lon = feature.get('lon') || feature.get('longitude'); const lat = feature.get('lat') || feature.get('latitude'); // You could show a popup or info panel here // For now, just log to console console.log('[App] Selected location:', { name, description, category, lon, lat }); // Optionally zoom to the location // mapView.zoomTo(lon, lat, 14); } function renderLocations(locations) { const container = document.getElementById('locations-list'); if (!container) return; // Also update mobile count const mobileCount = document.getElementById('location-count-mobile'); if (mobileCount) { mobileCount.textContent = locations.length; } if (locations.length === 0) { container.innerHTML = `
No locations yet.
Click the map or fill the form above!| Ready: | ${status.ready ? 'Yes' : 'No'} |
| Online: | ${isOnline() ? 'Yes' : 'Offline'} |
| Database: | ${status.databasePath || 'N/A'} |
| Tables: | ${status.tables.map(t => `${t}`).join('')} |
| Locations: | ${status.locationCount} |