/** * FLOCKRADAR - USER AUTHENTICATION & COMMUNITY STATE MANAGER * Waze-style driver profile, preferences, and crowdsourced reporting store. */ (function() { 'use strict'; const STORAGE_KEY_USER = 'flock_radar_user'; const STORAGE_KEY_REPORTS = 'flock_community_reports'; const STORAGE_KEY_METADATA = 'flock_camera_metadata'; const STORAGE_KEY_VOTES = 'flock_user_votes'; // Default Guest Profile const defaultUser = { isLoggedIn: false, username: 'Guest Driver', callsign: 'SCOUT-104', level: 1, points: 120, vehicle: 'Sedan (Silver)', soundEnabled: true, alertsMuted: false, reportsCount: 0, photosCount: 0 }; // State let currentUser = null; let communityReports = []; let cameraMetadata = {}; let userVotes = { confirmed: {}, contested: {} }; function init() { loadUser(); loadReports(); loadMetadata(); loadVotes(); } function loadUser() { try { const stored = localStorage.getItem(STORAGE_KEY_USER); if (stored) { currentUser = JSON.parse(stored); } else { currentUser = { ...defaultUser }; } } catch (e) { currentUser = { ...defaultUser }; } } function saveUser() { try { localStorage.setItem(STORAGE_KEY_USER, JSON.stringify(currentUser)); window.dispatchEvent(new CustomEvent('flock_user_updated', { detail: currentUser })); } catch (e) { console.error('Failed to save user session', e); } } function loadVotes() { try { const stored = localStorage.getItem(STORAGE_KEY_VOTES); userVotes = stored ? JSON.parse(stored) : { confirmed: {}, contested: {} }; } catch (e) { userVotes = { confirmed: {}, contested: {} }; } } function saveVotes() { try { localStorage.setItem(STORAGE_KEY_VOTES, JSON.stringify(userVotes)); } catch (e) { console.error('Failed to save votes', e); } } function loadMetadata() { try { const stored = localStorage.getItem(STORAGE_KEY_METADATA); cameraMetadata = stored ? JSON.parse(stored) : {}; } catch (e) { cameraMetadata = {}; } } function saveMetadata() { try { localStorage.setItem(STORAGE_KEY_METADATA, JSON.stringify(cameraMetadata)); window.dispatchEvent(new CustomEvent('flock_meta_updated', { detail: cameraMetadata })); } catch (e) { console.error('Failed to save camera metadata', e); } } function login(username, vehicle, soundEnabled) { currentUser = { isLoggedIn: true, username: username || 'Citizen Scout', callsign: 'RADAR-' + Math.floor(100 + Math.random() * 900), level: 2, points: (currentUser ? currentUser.points : 0) + 100, vehicle: vehicle || 'Standard Vehicle', soundEnabled: soundEnabled !== false, alertsMuted: false, reportsCount: communityReports.length, photosCount: currentUser ? (currentUser.photosCount || 0) : 0 }; saveUser(); return currentUser; } function logout() { currentUser = { ...defaultUser }; saveUser(); } function loadReports() { try { const stored = localStorage.getItem(STORAGE_KEY_REPORTS); if (stored) { communityReports = JSON.parse(stored); } else { communityReports = []; } } catch (e) { communityReports = []; } } function saveReports() { try { localStorage.setItem(STORAGE_KEY_REPORTS, JSON.stringify(communityReports)); window.dispatchEvent(new CustomEvent('flock_reports_updated', { detail: communityReports })); } catch (e) { console.error('Failed to save community reports', e); } } function addReport(report) { const reportId = 'rpt-' + Date.now(); const newReport = { id: reportId, lat: report.lat, lng: report.lng, type: report.type || 'Solar Pole Mount', intersection: report.intersection || 'Reported Location', notes: report.notes || 'Spotted by community driver', reportedBy: currentUser ? (currentUser.callsign || currentUser.username) : 'Anonymous Scout', timestamp: new Date().toISOString(), confirmations: 1, photoUrl: report.photoUrl || null }; communityReports.push(newReport); saveReports(); // If report has photo, initialize metadata if (report.photoUrl) { cameraMetadata[reportId] = { confirmations: 1, hasPhoto: true, photoUrl: report.photoUrl, photoDate: new Date().toISOString(), photoAuthor: currentUser ? (currentUser.callsign || currentUser.username) : 'Scout', isContested: false, contests: [] }; saveMetadata(); } if (currentUser) { currentUser.points = (currentUser.points || 0) + (report.photoUrl ? 150 : 50); currentUser.reportsCount = (currentUser.reportsCount || 0) + 1; if (report.photoUrl) { currentUser.photosCount = (currentUser.photosCount || 0) + 1; } saveUser(); } return newReport; } // --- CAMERA METADATA, CONFIRMATION & PHOTO PROOF API --- function getCameraMeta(camId) { if (!cameraMetadata[camId]) { // Default baseline cameraMetadata[camId] = { confirmations: 3, // Baseline community confirmations for directory portals hasPhoto: false, photoUrl: null, photoDate: null, photoAuthor: null, isContested: false, contests: [] }; } return cameraMetadata[camId]; } function confirmCamera(camId) { if (userVotes.confirmed[camId]) { return { success: false, message: 'You have already confirmed this camera is active!' }; } const meta = getCameraMeta(camId); meta.confirmations = (meta.confirmations || 0) + 1; userVotes.confirmed[camId] = new Date().toISOString(); saveMetadata(); saveVotes(); if (currentUser) { currentUser.points = (currentUser.points || 0) + 25; saveUser(); } return { success: true, count: meta.confirmations }; } function addPhotoProof(camId, photoDataUrl, notes) { const meta = getCameraMeta(camId); meta.hasPhoto = true; meta.photoUrl = photoDataUrl; meta.photoDate = new Date().toISOString(); meta.photoAuthor = currentUser ? (currentUser.callsign || currentUser.username) : 'Driver Scout'; meta.confirmations = (meta.confirmations || 0) + 2; // Adding photo boosts verification confidence saveMetadata(); if (currentUser) { currentUser.points = (currentUser.points || 0) + 100; currentUser.photosCount = (currentUser.photosCount || 0) + 1; saveUser(); } return { success: true, meta }; } function contestCamera(camId, reason, notes) { if (userVotes.contested[camId]) { return { success: false, message: 'You have already submitted a report for this location.' }; } const meta = getCameraMeta(camId); if (!meta.contests) meta.contests = []; meta.contests.push({ reason: reason || 'Camera reported missing', notes: notes || '', author: currentUser ? (currentUser.callsign || currentUser.username) : 'Scout', timestamp: new Date().toISOString() }); userVotes.contested[camId] = new Date().toISOString(); // Anti-Sabotage Rule: It requires at least 3 distinct driver contests to flag a camera if (meta.contests.length >= 3) { meta.isContested = true; } saveMetadata(); saveVotes(); return { success: true, contestCount: meta.contests.length, isContested: meta.isContested }; } function hasUserConfirmed(camId) { return !!(userVotes.confirmed && userVotes.confirmed[camId]); } function hasUserContested(camId) { return !!(userVotes.contested && userVotes.contested[camId]); } // --- CLIENT-SIDE FAST IMAGE COMPRESSION (CANVAS) --- // Compresses 4MB-10MB phone camera photos down to ~60-80KB WebP/JPEG in milliseconds function compressImage(file, maxWidth = 1000, quality = 0.72) { return new Promise((resolve, reject) => { if (!file || !file.type.startsWith('image/')) { return reject(new Error('Invalid image file.')); } const reader = new FileReader(); reader.onload = e => { const img = new Image(); img.onload = () => { try { const canvas = document.createElement('canvas'); let width = img.width; let height = img.height; if (width > maxWidth) { height = Math.round((height * maxWidth) / width); width = maxWidth; } canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); // Export as WebP if supported, fallback to JPEG let dataUrl = canvas.toDataURL('image/webp', quality); if (!dataUrl || dataUrl.indexOf('data:image/webp') !== 0) { dataUrl = canvas.toDataURL('image/jpeg', quality); } resolve(dataUrl); } catch (err) { reject(err); } }; img.onerror = () => reject(new Error('Failed to decode image data.')); img.src = e.target.result; }; reader.onerror = () => reject(new Error('Failed to read file.')); reader.readAsDataURL(file); }); } function getUser() { if (!currentUser) loadUser(); return currentUser; } function getReports() { if (!communityReports) loadReports(); return communityReports; } // Export API to global scope window.FlockAuth = { init, getUser, login, logout, saveUser, getReports, addReport, getCameraMeta, confirmCamera, addPhotoProof, contestCamera, hasUserConfirmed, hasUserContested, compressImage }; init(); })();