/** * 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!
`; return; } // Category emoji mapping const categoryEmojis = { 'water': '💧', 'school': '🏫', 'health': '🏥', 'market': '🏪', 'default': '📍', 'other': '📌' }; container.innerHTML = locations.map(loc => { const emoji = categoryEmojis[loc.category] || '📍'; return `
${emoji} ${escapeHtml(loc.name)}
${loc.latitude.toFixed(5)}, ${loc.longitude.toFixed(5)}
${loc.category}
${loc.description ? `${escapeHtml(loc.description)}` : ''}
`; }).join(''); // Add click handlers to zoom to location container.querySelectorAll('.location-item').forEach(item => { item.addEventListener('click', (e) => { e.preventDefault(); const lon = parseFloat(item.dataset.lon); const lat = parseFloat(item.dataset.lat); const id = parseInt(item.dataset.id); // Zoom to location on map mapView?.zoomTo(lon, lat, 14); // Select the marker mapView?.selectMarker(id); }); }); } // ============================================================================ // Export Handler // ============================================================================ async function handleExport() { try { await downloadDatabase('lupmis-backup.sqlite3'); showSuccess('Database exported successfully'); } catch (error) { console.error('[App] Export failed:', error); showError('Export failed: ' + error.message); } } // Export as GeoJSON file async function handleExportGeoJSON() { try { const geojson = await exportToGeoJSON(); // Download as file const blob = new Blob([JSON.stringify(geojson, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'locations.geojson'; a.click(); URL.revokeObjectURL(url); showSuccess(`Exported ${geojson.features.length} location(s)`); }catch (error) { console.error('[App] GeoJSON Export failed:', error); showError('GeoJSON Export failed: ' + error.message); } } // ============================================================================ // Status Handler // ============================================================================ async function handleShowStatus() { try { const status = await getDatabaseStatus(); // Update modal content const statusContent = document.getElementById('status-content'); if (statusContent) { statusContent.innerHTML = `
Ready: ${status.ready ? 'Yes' : 'No'}
Online: ${isOnline() ? 'Yes' : 'Offline'}
Database: ${status.databasePath || 'N/A'}
Tables: ${status.tables.map(t => `${t}`).join('')}
Locations: ${status.locationCount}
`; } // Show the modal using Bootstrap const statusModal = new Modal(document.getElementById('statusModal')); statusModal.show(); } catch (error) { console.error('[App] Failed to get status:', error); showError('Failed to get status'); } } // ============================================================================ // Sync (placeholder - implement based on your backend) // ============================================================================ async function syncData() { if (!isOnline()) { console.log('[App] Cannot sync - offline'); return; } // TODO: Implement sync with your backend // Example: // const unsynced = await getUnsyncedLocations(); // for (const location of unsynced) { // await fetch('/api/locations', { // method: 'POST', // body: JSON.stringify(location) // }); // await markLocationsSynced([location.id]); // } console.log('[App] Sync placeholder - implement based on your backend'); } // ============================================================================ // Utilities // ============================================================================ function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function showError(message) { const el = document.getElementById('error-message'); if (el) { el.querySelector('.message-text').textContent = message; el.classList.remove('d-none'); // Auto-hide after 5 seconds setTimeout(() => { el.classList.add('d-none'); }, 5000); } else { console.error(message); } } function showSuccess(message) { const el = document.getElementById('success-message'); if (el) { el.querySelector('.message-text').textContent = message; el.classList.remove('d-none'); // Auto-hide after 3 seconds setTimeout(() => { el.classList.add('d-none'); }, 3000); } else { console.log(message); } } // ============================================================================ // Start Application // ============================================================================ // Wait for DOM to be ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initApp); } else { initApp(); }