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 += `
No photos captured yet
Date: ${captureDate.toLocaleDateString()}
Time: ${captureDate.toLocaleTimeString()}
Image Size: ${metadata.image.width}×${metadata.image.height}
File Size: ${(metadata.image.size_bytes/1024).toFixed(1)} KB
Latitude: ${metadata.gps.latitude.toFixed(6)}
Longitude: ${metadata.gps.longitude.toFixed(6)}
Accuracy: ±${metadata.gps.accuracy.toFixed(0)}m
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}
` : ''}Screen Size: ${metadata.device.screen_size}
Camera: ${metadata.camera.facing_mode === 'environment' ? 'Back' : 'Front'}
Platform: ${metadata.device.platform}
Timezone: ${metadata.device.timezone}