1993 lines
82 KiB
JavaScript
1993 lines
82 KiB
JavaScript
/**
|
||
* FLOCKRADAR - LIVE MAPPING & PROXIMITY ALERT ENGINE
|
||
* Full-screen Waze-style radar map with red camera recording zones,
|
||
* real-time proximity calculation, audio warning synthesizer, and drive simulator.
|
||
*/
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
// --- CONFIGURATION & CAMERA CLUSTERS ---
|
||
// Key metropolitan corridors with realistic camera intersection networks
|
||
const DEFAULT_CAMERAS = [
|
||
// === PALMDALE & ANTELOPE VALLEY, CA (LOCAL SURVEILLANCE CORRIDORS) ===
|
||
{ id: 'cam-plm-1', name: 'Palmdale Blvd & 10th St W (SR-14 Commercial Corridor)', lat: 34.60194, lng: -118.14847, radius: 320, agency: 'City of Palmdale / LASD', slug: 'palmdale-ca-lasd', cameras: 6, captures30d: '385,000' },
|
||
{ id: 'cam-plm-2', name: 'Palmdale Blvd & 20th St E (East Gateway)', lat: 34.60230, lng: -118.10305, radius: 300, agency: 'City of Palmdale / LASD', slug: 'palmdale-ca-lasd', cameras: 4, captures30d: '295,000' },
|
||
{ id: 'cam-plm-3', name: 'Ave R & 47th St E (East Palmdale Hub)', lat: 34.58081, lng: -118.04497, radius: 300, agency: 'City of Palmdale / LASD', slug: 'palmdale-ca-lasd', cameras: 4, captures30d: '240,000' },
|
||
{ id: 'cam-plm-4', name: '10th St E & Ave R (Palmdale Civic Center)', lat: 34.57985, lng: -118.11677, radius: 280, agency: 'Palmdale Sheriff Station', slug: 'palmdale-ca-lasd', cameras: 4, captures30d: '310,000' },
|
||
{ id: 'cam-plm-5', name: 'Ave S & 10th St W (Pelona Vista / Retail)', lat: 34.56829, lng: -118.14702, radius: 300, agency: 'Los Angeles County Sheriff', slug: 'palmdale-ca-lasd', cameras: 4, captures30d: '330,000' },
|
||
{ id: 'cam-plm-6', name: 'Ave S & 25th St E', lat: 34.55799, lng: -118.08278, radius: 280, agency: 'Flock Safety / LASD', slug: 'palmdale-ca-lasd', cameras: 4, captures30d: '215,000' },
|
||
{ id: 'cam-plm-7', name: 'Ave S & 40th St E', lat: 34.55821, lng: -118.04760, radius: 280, agency: 'Flock Safety / LASD', slug: 'palmdale-ca-lasd', cameras: 4, captures30d: '190,000' },
|
||
{ id: 'cam-plm-8', name: 'Rancho Vista Blvd & 15th St W (AV Mall & Amphitheater)', lat: 34.63179, lng: -118.12594, radius: 350, agency: 'City of Palmdale / LASD', slug: 'palmdale-ca-lasd', cameras: 6, captures30d: '410,000' },
|
||
{ id: 'cam-plm-9', name: 'Palmdale Blvd & Highland High / 30th St W', lat: 34.60164, lng: -118.18153, radius: 300, agency: 'City of Palmdale / LASD', slug: 'palmdale-ca-lasd', cameras: 4, captures30d: '260,000' },
|
||
{ id: 'cam-plm-10', name: 'Pearblossom Hwy & Sierra Hwy (South Gateway)', lat: 34.53625, lng: -118.08431, radius: 320, agency: 'Los Angeles County Sheriff', slug: 'palmdale-ca-lasd', cameras: 4, captures30d: '345,000' },
|
||
{ id: 'cam-plm-11', name: 'Technology Dr & 10th St W (Auto Center Hub)', lat: 34.59852, lng: -118.14467, radius: 280, agency: 'Commercial ALPR Network', slug: 'palmdale-ca-lasd', cameras: 4, captures30d: '280,000' },
|
||
{ id: 'cam-plm-12', name: 'Sierra Hwy & Palmdale Blvd (Transit Station)', lat: 34.58259, lng: -118.13690, radius: 280, agency: 'Palmdale Transit Hub', slug: 'palmdale-ca-lasd', cameras: 4, captures30d: '290,000' },
|
||
|
||
// === LANCASTER, CA (ANTELOPE VALLEY NORTH) ===
|
||
{ id: 'cam-lan-1', name: 'Ave L & 10th St W (Lancaster Commercial)', lat: 34.69718, lng: -118.14770, radius: 320, agency: 'City of Lancaster / LASD', slug: 'lancaster-ca-lasd', cameras: 6, captures30d: '390,000' },
|
||
{ id: 'cam-lan-2', name: 'Ave K & 30th St W (Lancaster West)', lat: 34.68925, lng: -118.18368, radius: 300, agency: 'City of Lancaster / LASD', slug: 'lancaster-ca-lasd', cameras: 4, captures30d: '275,000' },
|
||
{ id: 'cam-lan-3', name: 'Ave J & 10th St W (Lancaster Central)', lat: 34.67398, lng: -118.14664, radius: 320, agency: 'City of Lancaster / LASD', slug: 'lancaster-ca-lasd', cameras: 6, captures30d: '360,000' },
|
||
{ id: 'cam-lan-4', name: 'The BLVD & Sierra Hwy (Lancaster Downtown)', lat: 34.69838, lng: -118.13727, radius: 280, agency: 'Lancaster Downtown BLVD', slug: 'lancaster-ca-lasd', cameras: 4, captures30d: '310,000' },
|
||
|
||
// === NATIONAL DEMO CORRIDORS (Lexington, KY) ===
|
||
{ id: 'cam-lex-1', name: 'Lexington PD - New Circle Rd & Nicholasville Rd', lat: 37.9942, lng: -84.5268, radius: 300, agency: 'Lexington Police Department', slug: 'lexington-ky-pd', cameras: 4, captures30d: '284,500' },
|
||
{ id: 'cam-lex-2', name: 'Lexington PD - Man o War Blvd & Harrodsburg Rd', lat: 37.9785, lng: -84.5582, radius: 320, agency: 'Lexington Police Department', slug: 'lexington-ky-pd', cameras: 6, captures30d: '312,000' },
|
||
{ id: 'cam-lex-3', name: 'Lexington PD - Richmond Rd & New Circle', lat: 38.0125, lng: -84.4715, radius: 280, agency: 'Lexington Police Department', slug: 'lexington-ky-pd', cameras: 4, captures30d: '265,100' },
|
||
{ id: 'cam-lex-4', name: 'Lexington PD - Downtown High St & S Broadway', lat: 38.0435, lng: -84.5020, radius: 250, agency: 'Lexington Police Department', slug: 'lexington-ky-pd', cameras: 2, captures30d: '198,400' },
|
||
{ id: 'cam-lex-5', name: 'Lexington PD - Russell Cave Rd & I-75', lat: 38.0815, lng: -84.4710, radius: 350, agency: 'Lexington Police Department', slug: 'lexington-ky-pd', cameras: 8, captures30d: '450,200' },
|
||
|
||
// Atlanta / Cobb County / Marietta, GA
|
||
{ id: 'cam-atl-1', name: 'Cobb County SO - Cobb Pkwy & Windy Hill Rd', lat: 33.9052, lng: -84.4789, radius: 350, agency: 'Cobb County Sheriff', slug: 'cobb-county-ga', cameras: 6, captures30d: '420,000' },
|
||
{ id: 'cam-atl-2', name: 'Marietta PD - Roswell St & Fairground St', lat: 33.9482, lng: -84.5365, radius: 300, agency: 'Marietta Police Department', slug: 'marietta-ga-pd', cameras: 4, captures30d: '290,000' },
|
||
{ id: 'cam-atl-3', name: 'Sandy Springs PD - Roswell Rd & Abernathy Rd', lat: 33.9312, lng: -84.3789, radius: 320, agency: 'Sandy Springs Police', slug: 'sandy-springs-ga-pd', cameras: 4, captures30d: '340,000' },
|
||
|
||
// California - Bay Area & Los Angeles
|
||
{ id: 'cam-cal-1', name: 'Vallejo PD - Sonoma Blvd & Redwood St', lat: 38.1125, lng: -122.2541, radius: 320, agency: 'Vallejo Police Department', slug: 'vallejo-ca-pd', cameras: 4, captures30d: '310,000' },
|
||
{ id: 'cam-cal-2', name: 'San Mateo County SO - El Camino Real & 5th Ave', lat: 37.5612, lng: -122.3215, radius: 300, agency: 'San Mateo County SO', slug: 'san-mateo-ca-so', cameras: 4, captures30d: '280,000' },
|
||
{ id: 'cam-cal-3', name: 'Glendale PD - Brand Blvd & Colorado St', lat: 34.1445, lng: -118.2555, radius: 280, agency: 'Glendale Police Department', slug: 'glendale-az-pd', cameras: 4, captures30d: '260,000' },
|
||
|
||
// Texas - Fort Worth / Dallas Metro
|
||
{ id: 'cam-dfw-1', name: 'Fort Worth PD - W 7th St & University Dr', lat: 32.7512, lng: -97.3562, radius: 320, agency: 'Fort Worth Police Department', slug: 'fort-worth-tx-pd', cameras: 6, captures30d: '390,000' },
|
||
{ id: 'cam-dfw-2', name: 'Fort Worth PD - Camp Bowie Blvd & Montgomery', lat: 32.7485, lng: -97.3685, radius: 300, agency: 'Fort Worth Police Department', slug: 'fort-worth-tx-pd', cameras: 4, captures30d: '310,000' }
|
||
];
|
||
|
||
// Simulated Drive Route: Palmdale Boulevard Corridor (Heading East past 10th St W & Sierra Hwy)
|
||
const SIM_ROUTE = [
|
||
{ lat: 34.60194, lng: -118.1680, speed: 45, label: 'Palmdale Blvd - Approaching 15th St W' },
|
||
{ lat: 34.60194, lng: -118.1580, speed: 42, label: 'Palmdale Blvd - Approaching SR-14 Freeway' },
|
||
{ lat: 34.60194, lng: -118.1525, speed: 38, label: 'ENTERING FLOCK CAMERA RECORDED ZONE' },
|
||
{ lat: 34.60194, lng: -118.14847, speed: 30, label: 'AT FLOCK CAMERA INTERSECTION (Palmdale Blvd & 10th St W)' },
|
||
{ lat: 34.60194, lng: -118.1420, speed: 35, label: 'Passing 5th St W' },
|
||
{ lat: 34.60194, lng: -118.1360, speed: 32, label: 'Approaching Sierra Hwy & Metrolink Crossing' },
|
||
{ lat: 34.60194, lng: -118.1250, speed: 40, label: 'Palmdale Blvd & 5th St E' },
|
||
{ lat: 34.60210, lng: -118.1150, speed: 42, label: 'Palmdale Blvd & 10th St E' },
|
||
{ lat: 34.60220, lng: -118.1080, speed: 38, label: 'Approaching East Palmdale Surveillance Grid' },
|
||
{ lat: 34.60230, lng: -118.10305, speed: 32, label: 'AT 20TH ST E FLOCK RECORDING INTERSECTION' },
|
||
{ lat: 34.60230, lng: -118.0950, speed: 45, label: 'Heading East towards 30th St E' }
|
||
];
|
||
|
||
// Default Location: Palmdale, California
|
||
const PALMDALE_DEFAULT = { lat: 34.60194, lng: -118.14847 };
|
||
|
||
// --- STATE ---
|
||
let map = null;
|
||
let darkTileLayer = null;
|
||
let satelliteLayer = null;
|
||
let satelliteLabelsLayer = null;
|
||
let isSatelliteActive = false;
|
||
let allCameras = [];
|
||
let cameraCircles = [];
|
||
let userMarker = null;
|
||
let currentPosition = { ...PALMDALE_DEFAULT };
|
||
let currentSpeed = 0;
|
||
let isInZone = false;
|
||
let nearestCamDistanceFt = 9999;
|
||
let audioContext = null;
|
||
let lastChimeTime = 0;
|
||
let isSimulating = false;
|
||
let simIndex = 0;
|
||
let simAnimId = null;
|
||
let simProgress = 0;
|
||
|
||
// New 5% Driving Mode Enhancements
|
||
let isFollowCarLocked = true;
|
||
let radarRangeMode = 'city'; // 'city' (1,000 ft) or 'highway' (2,500 ft)
|
||
let lastCautionSpokenCamId = null;
|
||
|
||
// Photo Evidence & Consensus State
|
||
let activeModalCamera = null;
|
||
let photoUploadTarget = 'detail'; // 'detail' | 'report' | 'contest'
|
||
let reportPhotoPendingDataUrl = null;
|
||
let contestPhotoPendingDataUrl = null;
|
||
|
||
// --- DOM REFERENCES ---
|
||
const el = {
|
||
radarView: document.getElementById('radar-view'),
|
||
directoryView: document.getElementById('directory-view'),
|
||
navMapBtn: document.getElementById('nav-map-btn'),
|
||
navDirBtn: document.getElementById('nav-dir-btn'),
|
||
driverProfileBtn: document.getElementById('driver-profile-btn'),
|
||
userProfilePill: document.getElementById('user-profile-pill'),
|
||
|
||
// HUD Elements
|
||
hudZoneBadge: document.getElementById('hud-zone-badge'),
|
||
hudSpeedVal: document.getElementById('hud-speed-val'),
|
||
hudDistVal: document.getElementById('hud-dist-val'),
|
||
hudDistSub: document.getElementById('hud-dist-sub'),
|
||
hudCamAgency: document.getElementById('hud-cam-agency'),
|
||
hudBannerAlert: document.getElementById('hud-banner-alert'),
|
||
hudSpottedBadge: document.getElementById('hud-spotted-badge'),
|
||
hudSpottedText: document.getElementById('hud-spotted-text'),
|
||
hudGaugeState: document.getElementById('hud-gauge-state'),
|
||
hudSignalMeter: document.getElementById('hud-signal-meter'),
|
||
|
||
// Quick Location Buttons & Search
|
||
btnLocateMe: document.getElementById('btn-locate-me'),
|
||
btnLocPalmdale: document.getElementById('btn-loc-palmdale'),
|
||
btnLocLancaster: document.getElementById('btn-loc-lancaster'),
|
||
btnLocPalmdaleEast: document.getElementById('btn-loc-palmdale-east'),
|
||
btnLocPalmdaleSouth: document.getElementById('btn-loc-palmdale-south'),
|
||
btnLocRancho: document.getElementById('btn-loc-rancho'),
|
||
mapSearchForm: document.getElementById('map-search-form'),
|
||
mapSearchInput: document.getElementById('map-search-input'),
|
||
|
||
// Sensitivity & Driving Controls
|
||
btnSensCity: document.getElementById('btn-sens-city'),
|
||
btnSensHwy: document.getElementById('btn-sens-hwy'),
|
||
btnFollowCar: document.getElementById('btn-follow-car'),
|
||
btnFullscreenMount: document.getElementById('btn-fullscreen-mount'),
|
||
btnRecenterCar: document.getElementById('btn-recenter-car'),
|
||
btnQuickLogCam: document.getElementById('btn-quick-log-cam'),
|
||
|
||
// Controls
|
||
btnLiveGps: document.getElementById('btn-live-gps'),
|
||
btnMapLayer: document.getElementById('btn-map-layer'),
|
||
btnTestCamera: document.getElementById('btn-test-camera'),
|
||
btnTestAlert: document.getElementById('btn-test-alert'),
|
||
btnSimDrive: document.getElementById('btn-sim-drive'),
|
||
btnResetSim: document.getElementById('btn-reset-sim'),
|
||
btnMuteSound: document.getElementById('btn-mute-sound'),
|
||
btnMobileConnect: document.getElementById('btn-mobile-connect'),
|
||
btnReportCameraFab: document.getElementById('btn-report-camera-fab'),
|
||
|
||
// Modals
|
||
loginModal: document.getElementById('login-modal'),
|
||
reportModal: document.getElementById('report-modal'),
|
||
loginForm: document.getElementById('login-form'),
|
||
reportForm: document.getElementById('report-form'),
|
||
loginCloseBtn: document.getElementById('login-close-btn'),
|
||
reportCloseBtn: document.getElementById('report-close-btn'),
|
||
loginGuestBtn: document.getElementById('login-guest-btn'),
|
||
|
||
// Hardware Proof & Verification Modals
|
||
cameraDetailModal: document.getElementById('camera-detail-modal'),
|
||
camDetailTitle: document.getElementById('cam-detail-title'),
|
||
camDetailVerifyBadge: document.getElementById('cam-detail-verify-badge'),
|
||
camDetailVerifyText: document.getElementById('cam-detail-verify-text'),
|
||
camDetailMeta: document.getElementById('cam-detail-meta'),
|
||
camProofContainer: document.getElementById('cam-proof-container'),
|
||
specMountType: document.getElementById('spec-mount-type'),
|
||
specAgency: document.getElementById('spec-agency'),
|
||
specCaptures: document.getElementById('spec-captures'),
|
||
specCoords: document.getElementById('spec-coords'),
|
||
btnVoteConfirmCam: document.getElementById('btn-vote-confirm-cam'),
|
||
btnVoteConfirmText: document.getElementById('btn-vote-confirm-text'),
|
||
btnProofAddPhoto: document.getElementById('btn-proof-add-photo'),
|
||
btnProofPhotoText: document.getElementById('btn-proof-photo-text'),
|
||
btnProofContest: document.getElementById('btn-proof-contest'),
|
||
btnProofNavigate: document.getElementById('btn-proof-navigate'),
|
||
btnProofOpenPortal: document.getElementById('btn-proof-open-portal'),
|
||
camDetailCloseBtn: document.getElementById('cam-detail-close-btn'),
|
||
|
||
// Contest Modal
|
||
contestModal: document.getElementById('contest-modal'),
|
||
contestCloseBtn: document.getElementById('contest-close-btn'),
|
||
contestForm: document.getElementById('contest-form'),
|
||
btnContestAddPhoto: document.getElementById('btn-contest-add-photo'),
|
||
contestPhotoBtnText: document.getElementById('contest-photo-btn-text'),
|
||
contestPhotoPreviewBox: document.getElementById('contest-photo-preview-box'),
|
||
contestPhotoPreviewImg: document.getElementById('contest-photo-preview-img'),
|
||
btnContestRemovePhoto: document.getElementById('btn-contest-remove-photo'),
|
||
|
||
// Report Form Photo Proof
|
||
btnReportAddPhoto: document.getElementById('btn-report-add-photo'),
|
||
reportPhotoBtnText: document.getElementById('report-photo-btn-text'),
|
||
reportPhotoPreviewBox: document.getElementById('report-photo-preview-box'),
|
||
reportPhotoPreviewImg: document.getElementById('report-photo-preview-img'),
|
||
btnReportRemovePhoto: document.getElementById('btn-report-remove-photo'),
|
||
|
||
// Native Camera / File Input
|
||
cameraPhotoFileInput: document.getElementById('camera-photo-file-input'),
|
||
|
||
// Admin Placed Camera Controls
|
||
camAdminControlBox: document.getElementById('cam-admin-control-box'),
|
||
btnAdminDeleteCam: document.getElementById('btn-admin-delete-cam'),
|
||
adminPlacedCount: document.getElementById('admin-placed-count'),
|
||
adminPlacedCamsList: document.getElementById('admin-placed-cams-list'),
|
||
btnAdminClearAllPlaced: document.getElementById('btn-admin-clear-all-placed'),
|
||
btnAdminManagePins: document.getElementById('btn-admin-manage-pins')
|
||
};
|
||
|
||
// --- INITIALIZATION ---
|
||
function init() {
|
||
setupViewSwitching();
|
||
initLeafletMap();
|
||
loadAllCameraData();
|
||
renderCameraZones();
|
||
setupUserMarker();
|
||
setupHud();
|
||
bindEvents();
|
||
updateProfileDisplay();
|
||
|
||
// Auto-detect real location immediately (Palmdale / GPS)
|
||
attemptAutoLocate();
|
||
|
||
// Start proximity ticker (runs locally every 300ms, zero API cost)
|
||
setInterval(checkProximityToCameras, 300);
|
||
}
|
||
|
||
// --- NAVIGATION VIEW SWITCHER ---
|
||
function setupViewSwitching() {
|
||
el.navMapBtn.addEventListener('click', () => switchView('map'));
|
||
el.navDirBtn.addEventListener('click', () => switchView('dir'));
|
||
}
|
||
|
||
function switchView(view) {
|
||
if (view === 'map') {
|
||
el.radarView.style.display = 'block';
|
||
el.directoryView.style.display = 'none';
|
||
el.navMapBtn.classList.add('active');
|
||
el.navDirBtn.classList.remove('active');
|
||
|
||
setTimeout(() => {
|
||
if (map) map.invalidateSize();
|
||
}, 100);
|
||
} else {
|
||
el.radarView.style.display = 'none';
|
||
el.directoryView.style.display = 'block';
|
||
el.navMapBtn.classList.remove('active');
|
||
el.navDirBtn.classList.add('active');
|
||
}
|
||
}
|
||
|
||
// --- LEAFLET MAP SETUP (HIGH-RESOLUTION ZERO-WATERMARK RADAR DARK MODE) ---
|
||
function initLeafletMap() {
|
||
// Center initially on Palmdale, California
|
||
map = L.map('radar-map', {
|
||
center: [34.5950, -118.1350],
|
||
zoom: 14,
|
||
zoomControl: false,
|
||
attributionControl: false
|
||
});
|
||
|
||
// High-Resolution Radar Dark Tiles (Zero API Key, Zero Watermarks, Sub-Meter Street Clarity up to Zoom 19)
|
||
darkTileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||
subdomains: 'abc',
|
||
maxZoom: 19,
|
||
className: 'dark-radar-tile',
|
||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||
}).addTo(map);
|
||
|
||
// Prepare optional Satellite Layer (Zero API Key, Zero Watermarks)
|
||
satelliteLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||
maxNativeZoom: 18,
|
||
maxZoom: 19,
|
||
attribution: '© Esri, Maxar, Earthstar Geographics'
|
||
});
|
||
|
||
// High-contrast street labels overlay for satellite view (Zero Watermarks)
|
||
satelliteLabelsLayer = L.tileLayer('https://services.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}', {
|
||
maxNativeZoom: 18,
|
||
maxZoom: 19
|
||
});
|
||
|
||
// Minimal zoom controls on bottom-left
|
||
L.control.zoom({ position: 'bottomleft' }).addTo(map);
|
||
|
||
// When the user drags the map, pause auto-centering and show Recenter button
|
||
map.on('dragstart', () => {
|
||
if (isFollowCarLocked) {
|
||
isFollowCarLocked = false;
|
||
updateFollowCarUI();
|
||
}
|
||
});
|
||
}
|
||
|
||
// --- LOAD CAMERA DATA (DEFAULTS + VERIFIED COMMUNITY + MUNICIPAL HUBS) ---
|
||
function loadAllCameraData() {
|
||
// Purge any unverified test marks or pins without photo proof
|
||
if (window.FlockAuth && typeof window.FlockAuth.clearUnverifiedReports === 'function') {
|
||
window.FlockAuth.clearUnverifiedReports();
|
||
}
|
||
|
||
// Filter out any unverified user pins
|
||
allCameras = [...DEFAULT_CAMERAS].filter(c => !c.id.startsWith('usr-pin-'));
|
||
|
||
// Load crowdsourced reports from auth store (ONLY if photo verified!)
|
||
if (window.FlockAuth) {
|
||
const reports = window.FlockAuth.getReports();
|
||
reports.forEach(r => {
|
||
// Strict verification rule: Sighting MUST have verified photo proof!
|
||
if (!r || !r.photoUrl || r.id.startsWith('usr-pin-')) return;
|
||
|
||
allCameras.push({
|
||
id: r.id,
|
||
name: `Verified Sighting: ${r.type || 'Solar Pole Mount'}`,
|
||
lat: r.lat,
|
||
lng: r.lng,
|
||
radius: 280,
|
||
agency: 'Community Verified ALPR',
|
||
slug: '',
|
||
cameras: 1,
|
||
captures30d: 'Photo Verified Hardware',
|
||
isCommunity: true,
|
||
reportedBy: r.reportedBy,
|
||
notes: r.notes
|
||
});
|
||
});
|
||
}
|
||
|
||
// Load ALL 1,439 geocoded portals from national dataset as active radar zones
|
||
if (window.FLOCK_DATA && window.FLOCK_DATA.portals) {
|
||
window.FLOCK_DATA.portals.forEach(p => {
|
||
if (p.lat && p.lng && !allCameras.some(c => c.slug === p.slug)) {
|
||
const count = p.cameras || 4;
|
||
const radiusMeters = Math.min(600, Math.max(250, Math.round(Math.sqrt(count) * 45)));
|
||
allCameras.push({
|
||
id: 'portal-' + p.slug,
|
||
name: `${p.name} (${p.location}, ${p.state})`,
|
||
lat: p.lat,
|
||
lng: p.lng,
|
||
radius: radiusMeters,
|
||
agency: p.name,
|
||
slug: p.slug,
|
||
cameras: count,
|
||
captures30d: (p.vehicles_captured ? p.vehicles_captured.toLocaleString() : 'Active')
|
||
});
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// --- RENDER RED RADAR CIRCLES & CAMERA NODES ---
|
||
function renderCameraZones() {
|
||
// Clear existing
|
||
cameraCircles.forEach(c => map.removeLayer(c));
|
||
cameraCircles = [];
|
||
|
||
allCameras.forEach(cam => {
|
||
const meta = window.FlockAuth ? window.FlockAuth.getCameraMeta(cam.id) : { confirmations: 1, hasPhoto: false, isContested: false };
|
||
const hasPhoto = meta.hasPhoto && !!meta.photoUrl;
|
||
|
||
// 1. Red Radar Recording Zone Circle (Translucent buffer)
|
||
const radarCircle = L.circle([cam.lat, cam.lng], {
|
||
radius: cam.radius,
|
||
color: meta.isContested ? '#f59e0b' : '#ef4444',
|
||
weight: 1.5,
|
||
opacity: 0.8,
|
||
fillColor: meta.isContested ? '#f59e0b' : '#ef4444',
|
||
fillOpacity: meta.isContested ? 0.12 : 0.18,
|
||
className: 'flock-radar-circle'
|
||
}).addTo(map);
|
||
|
||
// 2. Camera Center Node Marker
|
||
const iconHtml = `
|
||
<div class="camera-map-pin ${cam.isCommunity ? 'community-pin' : ''} ${hasPhoto ? 'has-photo-pin' : ''} ${meta.isContested ? 'is-contested-pin' : ''}">
|
||
<div class="cam-pulse-wave"></div>
|
||
<div class="cam-dot-center">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2.5">
|
||
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path>
|
||
<circle cx="12" cy="13" r="4"></circle>
|
||
</svg>
|
||
</div>
|
||
${hasPhoto ? '<div class="pin-photo-dot" title="Photo Evidence Verified">📸</div>' : ''}
|
||
</div>
|
||
`;
|
||
|
||
const customIcon = L.divIcon({
|
||
html: iconHtml,
|
||
className: 'custom-cam-div-icon',
|
||
iconSize: [28, 28],
|
||
iconAnchor: [14, 14]
|
||
});
|
||
|
||
const marker = L.marker([cam.lat, cam.lng], { icon: customIcon }).addTo(map);
|
||
|
||
// Popup details
|
||
const badgeText = meta.isContested
|
||
? '🟠 CONTESTED SIGHTING'
|
||
: (hasPhoto ? '🟢 PHOTO VERIFIED HARDWARE' : (cam.isCommunity ? 'COMMUNITY SIGHTING' : 'FLOCK RECORDED ZONE'));
|
||
|
||
const popupHtml = `
|
||
<div class="radar-popup-card">
|
||
<div class="popup-badge ${hasPhoto ? 'badge-verified-photo' : (meta.isContested ? 'badge-contested-tag' : '')}">${badgeText}</div>
|
||
<h4 class="popup-title">${escapeHtml(cam.name)}</h4>
|
||
<div class="popup-meta">
|
||
<span><strong>Agency:</strong> ${escapeHtml(cam.agency)}</span><br>
|
||
<span><strong>Consensus:</strong> 👍 ${meta.confirmations || 1} Confirmations</span><br>
|
||
<span><strong>Photo Proof:</strong> ${hasPhoto ? '🟢 Verified on Pole' : '⚠️ No Photo Yet (Needs Driver Sighting)'}</span>
|
||
${cam.notes ? `<br><span><em>${escapeHtml(cam.notes)}</em></span>` : ''}
|
||
</div>
|
||
<div style="display: flex; flex-direction: column; gap: 6px; margin-top: 10px;">
|
||
<button class="popup-inspect-btn" style="background: linear-gradient(135deg, #06b6d4, #0284c7); color: #fff; font-weight: 600;" onclick="window.openCameraHardwareProof('${escapeHtml(cam.id)}')">
|
||
📸 View Hardware Proof & Vote ↗
|
||
</button>
|
||
${cam.slug ? `
|
||
<button class="popup-inspect-btn" onclick="window.inspectPortal('${escapeHtml(cam.slug)}')">
|
||
Inspect Agency Records & Sharing ↗
|
||
</button>
|
||
` : ''}
|
||
${(cam.isCommunity || cam.id.startsWith('rpt-') || cam.id.startsWith('usr-pin-') || cam.id.startsWith('test-') || cam.id.startsWith('cam-test-')) ? `
|
||
<button class="popup-inspect-btn" style="background: rgba(239, 68, 68, 0.18); border: 1px solid #ef4444; color: #f87171;" onclick="window.adminDeleteCamera('${escapeHtml(cam.id)}')">
|
||
🗑️ Admin: Delete Placed Pin
|
||
</button>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
marker.bindPopup(popupHtml);
|
||
|
||
// Clicking marker directly opens the full hardware proof modal
|
||
marker.on('click', () => {
|
||
// Leaflet will open popup, but user can also click proof button directly
|
||
});
|
||
|
||
cameraCircles.push(radarCircle);
|
||
cameraCircles.push(marker);
|
||
});
|
||
}
|
||
|
||
// --- VEHICLE MARKER SETUP ---
|
||
function setupUserMarker() {
|
||
const vehicleHtml = `
|
||
<div class="user-vehicle-marker" id="user-vehicle-marker">
|
||
<div class="radar-scan-cone"></div>
|
||
<div class="car-body-dot">
|
||
<div class="car-arrow"></div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
const vehicleIcon = L.divIcon({
|
||
html: vehicleHtml,
|
||
className: 'custom-vehicle-div-icon',
|
||
iconSize: [40, 40],
|
||
iconAnchor: [20, 20]
|
||
});
|
||
|
||
userMarker = L.marker([currentPosition.lat, currentPosition.lng], {
|
||
icon: vehicleIcon,
|
||
zIndexOffset: 1000
|
||
}).addTo(map);
|
||
}
|
||
|
||
// --- HUD SETUP ---
|
||
function setupHud() {
|
||
el.hudSpeedVal.textContent = '0';
|
||
el.hudDistVal.textContent = '—';
|
||
el.hudZoneBadge.className = 'hud-badge badge-clear';
|
||
el.hudZoneBadge.innerHTML = `<span class="badge-dot dot-clear"></span> ALL CLEAR`;
|
||
el.hudCamAgency.textContent = 'Monitoring Highway Network';
|
||
}
|
||
|
||
// --- PROXIMITY CALCULATION & ALERT ENGINE ---
|
||
function checkProximityToCameras() {
|
||
if (!allCameras.length) return;
|
||
|
||
let closestDistMeters = Infinity;
|
||
let closestCam = null;
|
||
|
||
allCameras.forEach(cam => {
|
||
const dist = getHaversineDistanceMeters(currentPosition.lat, currentPosition.lng, cam.lat, cam.lng);
|
||
if (dist < closestDistMeters) {
|
||
closestDistMeters = dist;
|
||
closestCam = cam;
|
||
}
|
||
});
|
||
|
||
const distFt = Math.round(closestDistMeters * 3.28084);
|
||
nearestCamDistanceFt = distFt;
|
||
|
||
// Check if inside zone radius
|
||
const insideZone = closestDistMeters <= (closestCam.radius || 300);
|
||
|
||
// Update HUD
|
||
if (distFt < 5280) {
|
||
el.hudDistVal.textContent = distFt > 1000 ? (distFt / 1000).toFixed(1) + 'k' : distFt;
|
||
el.hudDistSub.textContent = 'FT';
|
||
} else {
|
||
el.hudDistVal.textContent = (distFt / 5280).toFixed(1);
|
||
el.hudDistSub.textContent = 'MI';
|
||
}
|
||
|
||
if (closestCam) {
|
||
el.hudCamAgency.textContent = closestCam.name;
|
||
}
|
||
|
||
// Update Graduated Radar Signal Strength Meter (Valentine One Style)
|
||
updateRadarGauge(distFt, insideZone, closestCam);
|
||
|
||
// State Transitions
|
||
const cautionThreshold = (radarRangeMode === 'highway') ? 2500 : 1200;
|
||
if (insideZone) {
|
||
if (!isInZone) {
|
||
// Just entered zone! Trigger Alert!
|
||
triggerZoneAlert(closestCam, distFt);
|
||
}
|
||
isInZone = true;
|
||
el.hudZoneBadge.className = 'hud-badge badge-danger';
|
||
el.hudZoneBadge.innerHTML = `<span class="badge-dot dot-danger"></span> IN RECORDED ZONE`;
|
||
el.hudBannerAlert.classList.add('visible');
|
||
el.hudBannerAlert.innerHTML = `
|
||
<span class="banner-pulse-icon">⚠️</span>
|
||
<div>
|
||
<strong>FLOCK CAMERA ACTIVE ZONE</strong>
|
||
<p>${escapeHtml(closestCam.agency)} • Vehicle Profile Logging</p>
|
||
</div>
|
||
`;
|
||
} else if (distFt <= cautionThreshold) {
|
||
// Approaching camera
|
||
isInZone = false;
|
||
el.hudZoneBadge.className = 'hud-badge badge-warning';
|
||
el.hudZoneBadge.innerHTML = `<span class="badge-dot dot-warning"></span> CAMERA AHEAD`;
|
||
el.hudBannerAlert.classList.remove('visible');
|
||
} else {
|
||
// Clear
|
||
isInZone = false;
|
||
el.hudZoneBadge.className = 'hud-badge badge-clear';
|
||
el.hudZoneBadge.innerHTML = `<span class="badge-dot dot-clear"></span> ALL CLEAR`;
|
||
el.hudBannerAlert.classList.remove('visible');
|
||
}
|
||
}
|
||
|
||
// --- 3-STAGE GRADUATED RADAR DETECTOR SIGNAL GAUGE ---
|
||
function updateRadarGauge(distFt, insideZone, closestCam) {
|
||
if (!el.hudGaugeState || !el.hudSignalMeter) return;
|
||
|
||
const isHighway = (radarRangeMode === 'highway');
|
||
const cautionDist = isHighway ? 2800 : 1500;
|
||
const warnDist = isHighway ? 1400 : 800;
|
||
|
||
const bars = el.hudSignalMeter.querySelectorAll('.signal-bar');
|
||
bars.forEach(b => {
|
||
b.className = 'signal-bar';
|
||
});
|
||
|
||
if (insideZone || distFt <= 450) {
|
||
// Level 5: Red Alert In Zone
|
||
el.hudGaugeState.className = 'hud-gauge-status status-red';
|
||
el.hudGaugeState.textContent = 'IN RECORDED ZONE!';
|
||
bars.forEach(b => b.classList.add('active-red'));
|
||
} else if (distFt <= warnDist) {
|
||
// Level 4: Immediate Proximity
|
||
el.hudGaugeState.className = 'hud-gauge-status status-red';
|
||
el.hudGaugeState.textContent = `ALERT: ${distFt} FT`;
|
||
bars[0].classList.add('active-red');
|
||
bars[1].classList.add('active-red');
|
||
bars[2].classList.add('active-red');
|
||
bars[3].classList.add('active-red');
|
||
} else if (distFt <= cautionDist) {
|
||
// Level 3: Approaching (Yellow Caution)
|
||
el.hudGaugeState.className = 'hud-gauge-status status-yellow';
|
||
el.hudGaugeState.textContent = `APPROACHING (${Math.round(distFt / 100) * 100} FT)`;
|
||
bars[0].classList.add('active-yellow');
|
||
bars[1].classList.add('active-yellow');
|
||
bars[2].classList.add('active-yellow');
|
||
|
||
// Pre-alert announcement once per camera approach
|
||
if (closestCam && lastCautionSpokenCamId !== closestCam.id) {
|
||
lastCautionSpokenCamId = closestCam.id;
|
||
playCautionBeep();
|
||
speakVoiceAlert(`Caution. Approaching surveillance camera in ${Math.round(distFt / 100) * 100} feet.`);
|
||
}
|
||
} else if (distFt <= (cautionDist * 1.6)) {
|
||
// Level 2: Grid Range
|
||
el.hudGaugeState.className = 'hud-gauge-status status-green';
|
||
el.hudGaugeState.textContent = 'MONITORING GRID';
|
||
bars[0].classList.add('active-green');
|
||
bars[1].classList.add('active-green');
|
||
if (closestCam && lastCautionSpokenCamId === closestCam.id && distFt > cautionDist + 500) {
|
||
lastCautionSpokenCamId = null; // Reset when moved away
|
||
}
|
||
} else {
|
||
// Level 1: All Clear
|
||
el.hudGaugeState.className = 'hud-gauge-status status-green';
|
||
el.hudGaugeState.textContent = 'ALL CLEAR';
|
||
bars[0].classList.add('active-green');
|
||
lastCautionSpokenCamId = null;
|
||
}
|
||
}
|
||
|
||
// --- AUDIO SYNTHESIZERS: CHIRPS & CAUTION BEEPS ---
|
||
function playLogConfirmationChirp() {
|
||
try {
|
||
if (!audioContext) {
|
||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||
}
|
||
if (audioContext.state === 'suspended') {
|
||
audioContext.resume();
|
||
}
|
||
const osc = audioContext.createOscillator();
|
||
const gain = audioContext.createGain();
|
||
osc.type = 'triangle';
|
||
osc.frequency.setValueAtTime(587.33, audioContext.currentTime); // D5
|
||
osc.frequency.setValueAtTime(880, audioContext.currentTime + 0.1); // A5
|
||
osc.frequency.setValueAtTime(1174.66, audioContext.currentTime + 0.2); // D6
|
||
gain.gain.setValueAtTime(0.35, audioContext.currentTime);
|
||
gain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.4);
|
||
osc.connect(gain);
|
||
gain.connect(audioContext.destination);
|
||
osc.start();
|
||
osc.stop(audioContext.currentTime + 0.4);
|
||
} catch (e) {
|
||
console.warn('Log chirp error:', e);
|
||
}
|
||
}
|
||
|
||
function playCautionBeep() {
|
||
const user = window.FlockAuth ? window.FlockAuth.getUser() : null;
|
||
if (user && !user.soundEnabled) return;
|
||
try {
|
||
if (!audioContext) {
|
||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||
}
|
||
if (audioContext.state === 'suspended') {
|
||
audioContext.resume();
|
||
}
|
||
const osc = audioContext.createOscillator();
|
||
const gain = audioContext.createGain();
|
||
osc.type = 'sine';
|
||
osc.frequency.setValueAtTime(520, audioContext.currentTime);
|
||
gain.gain.setValueAtTime(0.25, audioContext.currentTime);
|
||
gain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.22);
|
||
osc.connect(gain);
|
||
gain.connect(audioContext.destination);
|
||
osc.start();
|
||
osc.stop(audioContext.currentTime + 0.22);
|
||
} catch (e) {
|
||
console.warn('Caution beep error:', e);
|
||
}
|
||
}
|
||
|
||
// --- 1-TAP DRIVER QUICK-LOG CAMERA (IMMEDIATELY OPENS CAMERA FOR PHOTO PROOF) ---
|
||
function quickLogFlockCameraAtCurrentLocation() {
|
||
photoUploadTarget = 'quicklog';
|
||
if (el.cameraPhotoFileInput) {
|
||
el.cameraPhotoFileInput.value = '';
|
||
el.cameraPhotoFileInput.click();
|
||
}
|
||
}
|
||
|
||
// --- AUTO-CENTER / FOLLOW CAR HANDLER ---
|
||
function toggleFollowCar() {
|
||
isFollowCarLocked = !isFollowCarLocked;
|
||
updateFollowCarUI();
|
||
if (isFollowCarLocked) {
|
||
map.setView([currentPosition.lat, currentPosition.lng], Math.max(map.getZoom(), 15), { animate: true });
|
||
}
|
||
}
|
||
|
||
function recenterOnCar() {
|
||
isFollowCarLocked = true;
|
||
updateFollowCarUI();
|
||
map.setView([currentPosition.lat, currentPosition.lng], Math.max(map.getZoom(), 15), { animate: true });
|
||
}
|
||
|
||
function updateFollowCarUI() {
|
||
if (el.btnFollowCar) {
|
||
el.btnFollowCar.classList.toggle('active', isFollowCarLocked);
|
||
el.btnFollowCar.innerHTML = isFollowCarLocked
|
||
? `<span>🎯</span><span>Follow Car: ON</span>`
|
||
: `<span>🎯</span><span>Follow Car: OFF</span>`;
|
||
}
|
||
if (el.btnRecenterCar) {
|
||
el.btnRecenterCar.classList.toggle('hidden', isFollowCarLocked);
|
||
}
|
||
}
|
||
|
||
// --- ALERT SENSITIVITY RANGE (CITY vs HIGHWAY) ---
|
||
function setRadarSensitivity(mode) {
|
||
radarRangeMode = mode;
|
||
if (el.btnSensCity) el.btnSensCity.classList.toggle('active', mode === 'city');
|
||
if (el.btnSensHwy) el.btnSensHwy.classList.toggle('active', mode === 'highway');
|
||
playCautionBeep();
|
||
showToast(mode === 'highway' ? '🛣️ Highway Range Active (2,500 ft advance alert)' : '🏙️ City Range Active (1,000 ft alert)');
|
||
}
|
||
|
||
// --- DASHBOARD FULLSCREEN MOUNT MODE ---
|
||
function toggleFullscreenMount() {
|
||
if (!document.fullscreenElement) {
|
||
document.documentElement.requestFullscreen().then(() => {
|
||
if (el.btnFullscreenMount) el.btnFullscreenMount.classList.add('active');
|
||
showToast("📱 Fullscreen Dashboard Mode Active");
|
||
}).catch(err => {
|
||
console.warn('Fullscreen request failed:', err);
|
||
});
|
||
} else {
|
||
if (document.exitFullscreen) {
|
||
document.exitFullscreen();
|
||
if (el.btnFullscreenMount) el.btnFullscreenMount.classList.remove('active');
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- AUDIO SYNTHESIZER & SPOKEN VOICE ALERTS ---
|
||
function playRadarChime() {
|
||
const user = window.FlockAuth ? window.FlockAuth.getUser() : null;
|
||
if (user && !user.soundEnabled) return;
|
||
|
||
const now = Date.now();
|
||
if (now - lastChimeTime < 3000) return;
|
||
lastChimeTime = now;
|
||
|
||
try {
|
||
if (!audioContext) {
|
||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||
}
|
||
if (audioContext.state === 'suspended') {
|
||
audioContext.resume();
|
||
}
|
||
|
||
// Synthesize clean two-tone radar alert
|
||
const osc = audioContext.createOscillator();
|
||
const gain = audioContext.createGain();
|
||
|
||
osc.type = 'sine';
|
||
osc.frequency.setValueAtTime(880, audioContext.currentTime); // High tone
|
||
osc.frequency.setValueAtTime(659.25, audioContext.currentTime + 0.12); // Tone 2
|
||
osc.frequency.setValueAtTime(880, audioContext.currentTime + 0.24); // Return tone
|
||
|
||
gain.gain.setValueAtTime(0.35, audioContext.currentTime);
|
||
gain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.45);
|
||
|
||
osc.connect(gain);
|
||
gain.connect(audioContext.destination);
|
||
|
||
osc.start();
|
||
osc.stop(audioContext.currentTime + 0.45);
|
||
} catch (e) {
|
||
console.warn('Audio chime error:', e);
|
||
}
|
||
}
|
||
|
||
function speakVoiceAlert(text) {
|
||
const user = window.FlockAuth ? window.FlockAuth.getUser() : null;
|
||
if (user && !user.soundEnabled) return;
|
||
if (!window.speechSynthesis) return;
|
||
|
||
try {
|
||
window.speechSynthesis.cancel();
|
||
const utter = new SpeechSynthesisUtterance(text);
|
||
utter.rate = 1.05;
|
||
utter.pitch = 1.0;
|
||
utter.volume = 1.0;
|
||
window.speechSynthesis.speak(utter);
|
||
} catch (e) {
|
||
console.warn('Speech error:', e);
|
||
}
|
||
}
|
||
|
||
function triggerZoneAlert(cam, distFt) {
|
||
playRadarChime();
|
||
speakVoiceAlert("Warning. Flock camera detected ahead. You are entering a recorded surveillance zone.");
|
||
}
|
||
|
||
// --- TEST ALERT SOUND & VOICE NOW ---
|
||
function testAlertSoundAndVoice() {
|
||
playRadarChime();
|
||
speakVoiceAlert("Warning. Flock camera active zone ahead. You are being recorded.");
|
||
el.hudBannerAlert.classList.add('visible');
|
||
el.hudBannerAlert.innerHTML = `
|
||
<span class="banner-pulse-icon">⚠️</span>
|
||
<div>
|
||
<strong>FLOCK CAMERA ACTIVE ZONE</strong>
|
||
<p>Test Alert • Vehicle Profile Logging Active</p>
|
||
</div>
|
||
`;
|
||
setTimeout(() => {
|
||
el.hudBannerAlert.classList.remove('visible');
|
||
}, 4500);
|
||
}
|
||
|
||
// --- PLACE TEST CAMERA 400FT AHEAD OF USER ---
|
||
function placeTestCameraAhead() {
|
||
// Offset approx 120m (400 ft) north-east of current location
|
||
const testCam = {
|
||
id: 'cam-test-' + Date.now(),
|
||
name: 'Test Flock Camera (400ft Ahead)',
|
||
lat: currentPosition.lat + 0.0011,
|
||
lng: currentPosition.lng + 0.0003,
|
||
radius: 260,
|
||
agency: 'Local Test Camera Deployment',
|
||
slug: '',
|
||
cameras: 2,
|
||
captures30d: 'Active Demonstration Zone',
|
||
isCommunity: true,
|
||
notes: 'Manually dropped test camera to verify real-time driving alerts.'
|
||
};
|
||
|
||
allCameras.push(testCam);
|
||
renderCameraZones();
|
||
map.flyTo([testCam.lat, testCam.lng], 16, { duration: 1 });
|
||
|
||
playRadarChime();
|
||
speakVoiceAlert('Test camera placed 400 feet ahead. Move forward into the red circle to test the alert.');
|
||
alert('📍 Test Flock camera placed 400 feet ahead of you!\n\nWalk or drive forward into the red circle zone to trigger the live warning chime and voice alert.');
|
||
}
|
||
|
||
// --- LIVE GPS CONTINUOUS TRACKING ---
|
||
let liveGpsWatchId = null;
|
||
|
||
function toggleLiveGps() {
|
||
if (!navigator.geolocation) {
|
||
alert('Geolocation is not supported by your browser.');
|
||
return;
|
||
}
|
||
|
||
if (liveGpsWatchId !== null) {
|
||
navigator.geolocation.clearWatch(liveGpsWatchId);
|
||
liveGpsWatchId = null;
|
||
el.btnLiveGps.classList.remove('active');
|
||
el.btnLiveGps.innerHTML = `<span>🎯</span><span>Live GPS Drive Mode</span>`;
|
||
el.hudSpeedVal.textContent = '0';
|
||
return;
|
||
}
|
||
|
||
if (isSimulating) stopDriveSimulation();
|
||
|
||
el.btnLiveGps.classList.add('active');
|
||
el.btnLiveGps.innerHTML = `<span>🛑</span><span>Stop Live GPS</span>`;
|
||
speakVoiceAlert('Live GPS drive mode activated. Monitoring for Flock cameras.');
|
||
|
||
liveGpsWatchId = navigator.geolocation.watchPosition(
|
||
pos => {
|
||
currentPosition = {
|
||
lat: pos.coords.latitude,
|
||
lng: pos.coords.longitude
|
||
};
|
||
|
||
// Speed in mph
|
||
if (pos.coords.speed !== null && !isNaN(pos.coords.speed)) {
|
||
currentSpeed = Math.round(pos.coords.speed * 2.23694);
|
||
} else {
|
||
currentSpeed = 0;
|
||
}
|
||
el.hudSpeedVal.textContent = currentSpeed;
|
||
|
||
// Heading
|
||
if (pos.coords.heading !== null && !isNaN(pos.coords.heading)) {
|
||
const carElem = document.getElementById('user-vehicle-marker');
|
||
if (carElem) carElem.style.transform = `rotate(${pos.coords.heading}deg)`;
|
||
}
|
||
|
||
if (userMarker) userMarker.setLatLng([currentPosition.lat, currentPosition.lng]);
|
||
if (isFollowCarLocked) {
|
||
map.panTo([currentPosition.lat, currentPosition.lng], { animate: true, duration: 0.8 });
|
||
if (currentSpeed > 15 && map.getZoom() < 16) {
|
||
map.setZoom(16);
|
||
}
|
||
}
|
||
checkProximityToCameras();
|
||
},
|
||
err => {
|
||
console.warn('GPS Error:', err);
|
||
alert('GPS Location Error: ' + err.message + '\nPlease allow location access in your browser.');
|
||
},
|
||
{
|
||
enableHighAccuracy: true,
|
||
maximumAge: 1000,
|
||
timeout: 10000
|
||
}
|
||
);
|
||
}
|
||
|
||
// --- MOBILE PHONE CONNECT INSTRUCTIONS ---
|
||
function showMobileConnectModal() {
|
||
const httpUrl = 'http://192.168.1.249:8085';
|
||
const httpsUrl = 'https://192.168.1.249:8443';
|
||
alert(
|
||
"📱 HOW TO USE FLOCKRADAR ON YOUR PHONE IN ANTELOPE VALLEY:\n\n" +
|
||
"1. Connect your phone to your Wi-Fi or hotspot.\n\n" +
|
||
"2. FOR CONTINUOUS DRIVING GPS (iOS Safari & Android Chrome):\n" +
|
||
" Open: " + httpsUrl + "\n" +
|
||
" (Tap 'Show Details' / 'Advanced' -> 'Visit this website' to allow local SSL, then allow location access!)\n\n" +
|
||
"3. FOR INSTANT OVERVIEW: Open " + httpUrl + "\n\n" +
|
||
"4. Tap 'Auto-Detect (GPS)' and your device will track and alert you across all Antelope Valley Flock cameras!"
|
||
);
|
||
}
|
||
|
||
// --- DRIVE SIMULATOR ENGINE ---
|
||
function toggleDriveSimulation() {
|
||
if (isSimulating) {
|
||
stopDriveSimulation();
|
||
} else {
|
||
startDriveSimulation();
|
||
}
|
||
}
|
||
|
||
function startDriveSimulation() {
|
||
isSimulating = true;
|
||
el.btnSimDrive.innerHTML = `<span>⏸</span> Pause Simulation`;
|
||
el.btnSimDrive.classList.add('active');
|
||
|
||
simIndex = 0;
|
||
simProgress = 0;
|
||
currentPosition = { ...SIM_ROUTE[0] };
|
||
map.flyTo([currentPosition.lat, currentPosition.lng], 15, { animate: true, duration: 1 });
|
||
|
||
stepSimulation();
|
||
}
|
||
|
||
function stopDriveSimulation() {
|
||
isSimulating = false;
|
||
el.btnSimDrive.innerHTML = `<span>▶</span> Simulate Drive`;
|
||
el.btnSimDrive.classList.remove('active');
|
||
if (simAnimId) cancelAnimationFrame(simAnimId);
|
||
}
|
||
|
||
function resetDriveSimulation() {
|
||
stopDriveSimulation();
|
||
simIndex = 0;
|
||
simProgress = 0;
|
||
currentPosition = { ...SIM_ROUTE[0] };
|
||
currentSpeed = 0;
|
||
el.hudSpeedVal.textContent = '0';
|
||
if (userMarker) userMarker.setLatLng([currentPosition.lat, currentPosition.lng]);
|
||
map.panTo([currentPosition.lat, currentPosition.lng]);
|
||
checkProximityToCameras();
|
||
}
|
||
|
||
function stepSimulation() {
|
||
if (!isSimulating) return;
|
||
|
||
const p1 = SIM_ROUTE[simIndex];
|
||
const p2 = SIM_ROUTE[simIndex + 1];
|
||
|
||
if (!p2) {
|
||
// Reached end of demo route, loop smoothly
|
||
simIndex = 0;
|
||
simProgress = 0;
|
||
simAnimId = requestAnimationFrame(stepSimulation);
|
||
return;
|
||
}
|
||
|
||
// Step rate
|
||
simProgress += 0.004;
|
||
|
||
if (simProgress >= 1) {
|
||
simProgress = 0;
|
||
simIndex++;
|
||
} else {
|
||
// Interpolate lat / lng
|
||
currentPosition.lat = p1.lat + (p2.lat - p1.lat) * simProgress;
|
||
currentPosition.lng = p1.lng + (p2.lng - p1.lng) * simProgress;
|
||
currentSpeed = Math.round(p1.speed + (p2.speed - p1.speed) * simProgress);
|
||
|
||
// Update marker position
|
||
if (userMarker) userMarker.setLatLng([currentPosition.lat, currentPosition.lng]);
|
||
|
||
// Rotate car icon based on heading angle
|
||
const angle = getBearing(p1.lat, p1.lng, p2.lat, p2.lng);
|
||
const carElem = document.getElementById('user-vehicle-marker');
|
||
if (carElem) {
|
||
carElem.style.transform = `rotate(${angle}deg)`;
|
||
}
|
||
|
||
// Smoothly pan map along if follow car is locked
|
||
if (isFollowCarLocked) {
|
||
map.panTo([currentPosition.lat, currentPosition.lng], { animate: false });
|
||
}
|
||
|
||
// Update Speedometer
|
||
el.hudSpeedVal.textContent = currentSpeed;
|
||
}
|
||
|
||
simAnimId = requestAnimationFrame(stepSimulation);
|
||
}
|
||
|
||
// --- HAVERSINE FORMULA ---
|
||
function getHaversineDistanceMeters(lat1, lon1, lat2, lon2) {
|
||
const R = 6371e3; // Earth radius in meters
|
||
const phi1 = lat1 * Math.PI / 180;
|
||
const phi2 = lat2 * Math.PI / 180;
|
||
const deltaPhi = (lat2 - lat1) * Math.PI / 180;
|
||
const deltaLambda = (lon2 - lon1) * Math.PI / 180;
|
||
|
||
const a = Math.sin(deltaPhi / 2) * Math.sin(deltaPhi / 2) +
|
||
Math.cos(phi1) * Math.cos(phi2) *
|
||
Math.sin(deltaLambda / 2) * Math.sin(deltaLambda / 2);
|
||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||
|
||
return R * c;
|
||
}
|
||
|
||
function getBearing(startLat, startLng, destLat, destLng) {
|
||
const startLatRad = startLat * Math.PI / 180;
|
||
const startLngRad = startLng * Math.PI / 180;
|
||
const destLatRad = destLat * Math.PI / 180;
|
||
const destLngRad = destLng * Math.PI / 180;
|
||
|
||
const y = Math.sin(destLngRad - startLngRad) * Math.cos(destLatRad);
|
||
const x = Math.cos(startLatRad) * Math.sin(destLatRad) -
|
||
Math.sin(startLatRad) * Math.cos(destLatRad) * Math.cos(destLngRad - startLngRad);
|
||
let brng = Math.atan2(y, x) * 180 / Math.PI;
|
||
return (brng + 360) % 360;
|
||
}
|
||
|
||
// --- MULTI-TIER AUTO-LOCATION RESOLVER (ANTELOPE VALLEY & BEYOND) ---
|
||
async function attemptAutoLocate() {
|
||
updateSpottedBadge('🛰️ Spotting Antelope Valley Location...');
|
||
|
||
// Tier 1: Instant IP-based Network Geolocation
|
||
// Works 100% reliably even when browser GPS is blocked, disabled, or accessed over HTTP
|
||
let ipFound = false;
|
||
try {
|
||
const resp = await fetch('https://get.geojs.io/v1/ip/geo.json', { cache: 'no-cache' });
|
||
const ipData = await resp.json();
|
||
if (ipData && ipData.latitude && ipData.longitude) {
|
||
const ipLat = parseFloat(ipData.latitude);
|
||
const ipLng = parseFloat(ipData.longitude);
|
||
const city = ipData.city || 'Palmdale';
|
||
const region = ipData.region || 'California';
|
||
|
||
console.log('IP Network Geolocation spotted:', city, region, ipLat, ipLng);
|
||
|
||
// Center directly on Antelope Valley (Palmdale / Lancaster corridor)
|
||
let targetLat = ipLat;
|
||
let targetLng = ipLng;
|
||
const isAntelopeValley = city.toLowerCase().includes('palmdale') ||
|
||
city.toLowerCase().includes('lancaster') ||
|
||
(ipLat > 34.2 && ipLat < 34.85 && ipLng > -118.45 && ipLng < -117.8);
|
||
|
||
if (isAntelopeValley) {
|
||
// Snap directly to Palmdale central corridor (Palmdale Blvd & 10th St W)
|
||
targetLat = 34.60194;
|
||
targetLng = -118.14847;
|
||
}
|
||
|
||
updateUserCoordinates(targetLat, targetLng, 0, 0);
|
||
map.flyTo([targetLat, targetLng], 14, { duration: 1.2 });
|
||
updateSpottedBadge(`📍 Spotted: ${city}, Antelope Valley (Network)`);
|
||
updateHudLocationStatus(`Monitoring ${city} / Antelope Valley Corridors`);
|
||
speakVoiceAlert(`Location spotted: Antelope Valley, California.`);
|
||
checkProximityToCameras();
|
||
ipFound = true;
|
||
}
|
||
} catch (e) {
|
||
console.warn('Primary geojs IP lookup failed, trying backup:', e);
|
||
try {
|
||
const resp2 = await fetch('https://ipwho.is/', { cache: 'no-cache' });
|
||
const ipData2 = await resp2.json();
|
||
if (ipData2 && ipData2.success && ipData2.latitude) {
|
||
updateUserCoordinates(34.60194, -118.14847, 0, 0);
|
||
map.flyTo([34.60194, -118.14847], 14, { duration: 1.2 });
|
||
updateSpottedBadge(`📍 Spotted: ${ipData2.city || 'Antelope Valley'} (Network)`);
|
||
checkProximityToCameras();
|
||
ipFound = true;
|
||
}
|
||
} catch (err2) {
|
||
console.warn('Backup IP lookup failed:', err2);
|
||
}
|
||
}
|
||
|
||
if (!ipFound) {
|
||
updateSpottedBadge('📍 Antelope Valley: Palmdale, CA (Default)');
|
||
updateHudLocationStatus('Monitoring Antelope Valley Corridors');
|
||
checkProximityToCameras();
|
||
}
|
||
|
||
// Tier 2: Hardware Device GPS (Sub-meter accuracy if granted)
|
||
if (navigator.geolocation && (window.isSecureContext || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')) {
|
||
navigator.geolocation.getCurrentPosition(
|
||
pos => {
|
||
const lat = pos.coords.latitude;
|
||
const lng = pos.coords.longitude;
|
||
const acc = Math.round(pos.coords.accuracy || 10);
|
||
console.log('Hardware GPS lock acquired:', lat, lng, '±' + acc + 'm');
|
||
updateUserCoordinates(lat, lng, pos.coords.speed, pos.coords.heading);
|
||
map.flyTo([lat, lng], 15, { duration: 1.2 });
|
||
updateSpottedBadge(`🎯 High-Precision GPS (±${acc}m) • Antelope Valley`);
|
||
updateHudLocationStatus('📍 Live Driving GPS Active');
|
||
checkProximityToCameras();
|
||
},
|
||
gpsErr => {
|
||
console.log('Hardware GPS not granted/pending:', gpsErr.message);
|
||
// Retain the confirmed Antelope Valley IP location smoothly
|
||
},
|
||
{ enableHighAccuracy: true, timeout: 6000, maximumAge: 30000 }
|
||
);
|
||
}
|
||
}
|
||
|
||
function updateSpottedBadge(text) {
|
||
if (el.hudSpottedText) {
|
||
el.hudSpottedText.textContent = text;
|
||
}
|
||
}
|
||
|
||
function updateUserCoordinates(lat, lng, speed, heading) {
|
||
currentPosition.lat = lat;
|
||
currentPosition.lng = lng;
|
||
|
||
if (speed !== null && !isNaN(speed)) {
|
||
currentSpeed = Math.round(speed * 2.23694);
|
||
} else {
|
||
currentSpeed = 0;
|
||
}
|
||
el.hudSpeedVal.textContent = currentSpeed;
|
||
|
||
if (userMarker) {
|
||
userMarker.setLatLng([lat, lng]);
|
||
}
|
||
|
||
if (heading !== null && !isNaN(heading)) {
|
||
const carElem = document.getElementById('user-vehicle-marker');
|
||
if (carElem) carElem.style.transform = `rotate(${heading}deg)`;
|
||
}
|
||
}
|
||
|
||
function updateHudLocationStatus(statusText) {
|
||
if (el.hudCamAgency) {
|
||
el.hudCamAgency.textContent = statusText;
|
||
}
|
||
}
|
||
|
||
async function useLiveGps() {
|
||
if (isSimulating) stopDriveSimulation();
|
||
|
||
updateSpottedBadge('🛰️ Re-detecting location in Antelope Valley...');
|
||
updateHudLocationStatus('Acquiring high-precision location...');
|
||
|
||
// 1. First ensure IP location is refreshed
|
||
try {
|
||
const resp = await fetch('https://get.geojs.io/v1/ip/geo.json', { cache: 'no-cache' });
|
||
const ipData = await resp.json();
|
||
if (ipData && ipData.latitude) {
|
||
const city = ipData.city || 'Palmdale';
|
||
jumpToLocation(34.60194, -118.14847, `${city} (Antelope Valley)`);
|
||
updateSpottedBadge(`📍 Spotted: ${city}, Antelope Valley`);
|
||
}
|
||
} catch (e) {}
|
||
|
||
// 2. Hardware GPS Attempt
|
||
if (navigator.geolocation) {
|
||
navigator.geolocation.getCurrentPosition(
|
||
pos => {
|
||
updateUserCoordinates(pos.coords.latitude, pos.coords.longitude, pos.coords.speed, pos.coords.heading);
|
||
map.flyTo([pos.coords.latitude, pos.coords.longitude], 16, { duration: 1.2 });
|
||
const acc = Math.round(pos.coords.accuracy || 10);
|
||
updateSpottedBadge(`🎯 Live GPS Active (±${acc}m)`);
|
||
updateHudLocationStatus('📍 High-Precision GPS Locked');
|
||
playRadarChime();
|
||
speakVoiceAlert('GPS position locked in Antelope Valley.');
|
||
checkProximityToCameras();
|
||
},
|
||
err => {
|
||
console.warn('GPS hardware notice:', err.message);
|
||
// Show non-blocking informative banner
|
||
if (err.code === 1) { // PERMISSION_DENIED
|
||
el.hudBannerAlert.classList.add('visible');
|
||
el.hudBannerAlert.innerHTML = `
|
||
<span class="banner-pulse-icon">ℹ️</span>
|
||
<div>
|
||
<strong>ANTELOPE VALLEY NETWORK LOCATION ACTIVE</strong>
|
||
<p>To enable turn-by-turn driving GPS, click the lock icon 🔒 in your browser address bar and select 'Allow Location'.</p>
|
||
</div>
|
||
`;
|
||
setTimeout(() => {
|
||
el.hudBannerAlert.classList.remove('visible');
|
||
}, 6000);
|
||
}
|
||
},
|
||
{ enableHighAccuracy: true, timeout: 8000 }
|
||
);
|
||
}
|
||
}
|
||
|
||
// --- LOCATION PRESET JUMPS (ANTELOPE VALLEY & SUB-REGIONS) ---
|
||
function jumpToLocation(lat, lng, label, zoom = 15) {
|
||
if (isSimulating) stopDriveSimulation();
|
||
updateUserCoordinates(lat, lng, 0, 0);
|
||
map.flyTo([lat, lng], zoom, { duration: 1.2 });
|
||
updateHudLocationStatus(`📍 ${label}`);
|
||
updateSpottedBadge(`📍 Set to: ${label}`);
|
||
checkProximityToCameras();
|
||
}
|
||
|
||
// --- FREE NOMINATIM ADDRESS SEARCH (100% UNMETERED $0.00) ---
|
||
async function searchLocation(query) {
|
||
if (!query) return;
|
||
try {
|
||
updateHudLocationStatus(`Searching "${query}"...`);
|
||
const resp = await fetch(`https://nominatim.openstreetmap.org/search?format=json&limit=1&q=${encodeURIComponent(query)}`);
|
||
const data = await resp.json();
|
||
if (data && data.length > 0) {
|
||
const item = data[0];
|
||
const lat = parseFloat(item.lat);
|
||
const lng = parseFloat(item.lon);
|
||
const shortName = item.display_name.split(',').slice(0, 2).join(',');
|
||
jumpToLocation(lat, lng, shortName);
|
||
speakVoiceAlert(`Navigated to ${shortName}`);
|
||
} else {
|
||
alert('Location not found. Please try a street or city name (e.g. Palmdale Blvd, Ave S, Lancaster).');
|
||
}
|
||
} catch (e) {
|
||
console.warn('Search error:', e);
|
||
alert('Could not complete address search. Please check network connection.');
|
||
}
|
||
}
|
||
|
||
// --- MAP LAYER TOGGLE (DARK CANVAS vs SATELLITE HYBRID) ---
|
||
function toggleMapLayer() {
|
||
if (!map) return;
|
||
isSatelliteActive = !isSatelliteActive;
|
||
if (isSatelliteActive) {
|
||
if (darkTileLayer && map.hasLayer(darkTileLayer)) map.removeLayer(darkTileLayer);
|
||
if (satelliteLayer) satelliteLayer.addTo(map);
|
||
if (satelliteLabelsLayer) satelliteLabelsLayer.addTo(map);
|
||
el.btnMapLayer.innerHTML = `<span>🗺️</span><span>Dark Canvas</span>`;
|
||
el.btnMapLayer.classList.add('active');
|
||
} else {
|
||
if (satelliteLayer && map.hasLayer(satelliteLayer)) map.removeLayer(satelliteLayer);
|
||
if (satelliteLabelsLayer && map.hasLayer(satelliteLabelsLayer)) map.removeLayer(satelliteLabelsLayer);
|
||
if (darkTileLayer) darkTileLayer.addTo(map);
|
||
el.btnMapLayer.innerHTML = `<span>🛰️</span><span>Satellite / Dark</span>`;
|
||
el.btnMapLayer.classList.remove('active');
|
||
}
|
||
}
|
||
|
||
// --- USER PROFILE & LOGIN MODAL ---
|
||
function updateProfileDisplay() {
|
||
const user = window.FlockAuth ? window.FlockAuth.getUser() : null;
|
||
if (user && user.isLoggedIn) {
|
||
el.userProfilePill.innerHTML = `
|
||
<span class="avatar-dot">🚘</span>
|
||
<span>${escapeHtml(user.username)}</span>
|
||
<span class="points-badge">${user.points} pts</span>
|
||
`;
|
||
} else {
|
||
el.userProfilePill.innerHTML = `
|
||
<span class="avatar-dot">👤</span>
|
||
<span>Driver Login</span>
|
||
`;
|
||
}
|
||
}
|
||
|
||
function openLoginModal() {
|
||
const user = window.FlockAuth ? window.FlockAuth.getUser() : null;
|
||
if (user && user.isLoggedIn) {
|
||
// Pre-fill
|
||
document.getElementById('login-username').value = user.username;
|
||
document.getElementById('login-vehicle').value = user.vehicle;
|
||
}
|
||
|
||
renderAdminPlacedList();
|
||
|
||
if (typeof el.loginModal.showModal === 'function') {
|
||
el.loginModal.showModal();
|
||
} else {
|
||
el.loginModal.setAttribute('open', '');
|
||
}
|
||
}
|
||
|
||
function closeLoginModal() {
|
||
if (typeof el.loginModal.close === 'function') {
|
||
el.loginModal.close();
|
||
} else {
|
||
el.loginModal.removeAttribute('open');
|
||
}
|
||
}
|
||
|
||
// --- CROWDSOURCED REPORT MODAL ---
|
||
function openReportModal() {
|
||
// Fill current coordinates
|
||
document.getElementById('report-lat').value = currentPosition.lat.toFixed(5);
|
||
document.getElementById('report-lng').value = currentPosition.lng.toFixed(5);
|
||
|
||
if (typeof el.reportModal.showModal === 'function') {
|
||
el.reportModal.showModal();
|
||
} else {
|
||
el.reportModal.setAttribute('open', '');
|
||
}
|
||
}
|
||
|
||
function closeReportModal() {
|
||
if (typeof el.reportModal.close === 'function') {
|
||
el.reportModal.close();
|
||
} else {
|
||
el.reportModal.removeAttribute('open');
|
||
}
|
||
}
|
||
|
||
// --- SURVEILLANCE ZONE HARDWARE PROOF MODAL ---
|
||
function openCameraDetailModal(cam) {
|
||
if (!cam) return;
|
||
activeModalCamera = cam;
|
||
const meta = window.FlockAuth ? window.FlockAuth.getCameraMeta(cam.id) : { confirmations: 1, hasPhoto: false, isContested: false };
|
||
const hasPhoto = meta.hasPhoto && !!meta.photoUrl;
|
||
|
||
// Title & Agency
|
||
if (el.camDetailTitle) el.camDetailTitle.textContent = cam.name || 'Flock ALPR Surveillance Installation';
|
||
if (el.camDetailMeta) el.camDetailMeta.textContent = `${cam.agency || 'Law Enforcement'} • Monitored Highway Grid`;
|
||
|
||
// Verification Badge
|
||
if (el.camDetailVerifyBadge && el.camDetailVerifyText) {
|
||
if (meta.isContested) {
|
||
el.camDetailVerifyBadge.className = 'cam-verification-badge badge-contested';
|
||
el.camDetailVerifyText.textContent = `🟠 CONTESTED SIGHTING (${meta.contests ? meta.contests.length : 1} REPORTS)`;
|
||
} else if (hasPhoto) {
|
||
el.camDetailVerifyBadge.className = 'cam-verification-badge badge-verified';
|
||
el.camDetailVerifyText.textContent = `🟢 VERIFIED PHOTO PROOF • ${meta.confirmations || 1} CONFIRMATIONS`;
|
||
} else {
|
||
el.camDetailVerifyBadge.className = 'cam-verification-badge badge-unverified';
|
||
el.camDetailVerifyText.textContent = `🟡 UNVERIFIED PIN • NEEDS PHOTO PROOF (${meta.confirmations || 1} CONFIRMED)`;
|
||
}
|
||
}
|
||
|
||
// Photo Container
|
||
if (el.camProofContainer) {
|
||
if (hasPhoto) {
|
||
el.camProofContainer.innerHTML = `
|
||
<div class="proof-photo-box">
|
||
<img src="${meta.photoUrl}" class="proof-photo-img" alt="Flock camera mounted on pole" onclick="window.open('${meta.photoUrl}', '_blank')" title="Click to view full size">
|
||
<div class="proof-photo-overlay">
|
||
<div>
|
||
<span class="proof-photo-author">📸 Scout: ${escapeHtml(meta.photoAuthor || 'Active Driver')}</span><br>
|
||
<span class="proof-photo-date">Logged: ${new Date(meta.photoDate || Date.now()).toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' })}</span>
|
||
</div>
|
||
<span style="background: rgba(16,185,129,0.25); border: 1px solid #10b981; padding: 3px 8px; border-radius: 12px; font-size: 0.7rem; font-weight: 700; color: #34d399;">VERIFIED HARDWARE</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
if (el.btnProofPhotoText) el.btnProofPhotoText.textContent = 'Update Photo';
|
||
} else {
|
||
el.camProofContainer.innerHTML = `
|
||
<div class="proof-empty-box">
|
||
<div class="proof-empty-icon">📸</div>
|
||
<div class="proof-empty-title">No Physical Hardware Photo Yet</div>
|
||
<div class="proof-empty-desc">
|
||
Flock cameras stay permanently bolted to utility poles and traffic signals with small solar panels. Snap a photo of the pole to lock this camera into the permanent verified registry.
|
||
</div>
|
||
<button type="button" class="btn-proof-snap-cta" onclick="window.triggerPhotoCaptureForActiveCam()">
|
||
<span>📸</span>
|
||
<span>Snap Camera on Pole (+100 pts)</span>
|
||
</button>
|
||
</div>
|
||
`;
|
||
if (el.btnProofPhotoText) el.btnProofPhotoText.textContent = 'Add Photo Proof';
|
||
}
|
||
}
|
||
|
||
// Specs
|
||
if (el.specMountType) el.specMountType.textContent = cam.type || 'Solar Pole Mount (Falcon)';
|
||
if (el.specAgency) el.specAgency.textContent = cam.agency || 'Local Law Enforcement';
|
||
if (el.specCaptures) el.specCaptures.textContent = cam.captures30d ? `${cam.captures30d} Reads` : 'Active Corridor';
|
||
if (el.specCoords) el.specCoords.textContent = `${cam.lat.toFixed(4)}, ${cam.lng.toFixed(4)}`;
|
||
|
||
// Confirm button state
|
||
const userConfirmed = window.FlockAuth ? window.FlockAuth.hasUserConfirmed(cam.id) : false;
|
||
if (el.btnVoteConfirmCam && el.btnVoteConfirmText) {
|
||
if (userConfirmed) {
|
||
el.btnVoteConfirmCam.classList.add('voted');
|
||
el.btnVoteConfirmText.textContent = `👍 Confirmed (${meta.confirmations || 1})`;
|
||
} else {
|
||
el.btnVoteConfirmCam.classList.remove('voted');
|
||
el.btnVoteConfirmText.textContent = `Confirm Active (${meta.confirmations || 1})`;
|
||
}
|
||
}
|
||
|
||
// Navigate link
|
||
if (el.btnProofNavigate) {
|
||
el.btnProofNavigate.href = `https://www.google.com/maps/dir/?api=1&destination=${cam.lat},${cam.lng}`;
|
||
}
|
||
|
||
// Transparency Portal Link
|
||
const portalBox = document.getElementById('proof-portal-link-box');
|
||
if (portalBox) {
|
||
if (cam.slug) {
|
||
portalBox.style.display = 'block';
|
||
if (el.btnProofOpenPortal) {
|
||
el.btnProofOpenPortal.onclick = () => {
|
||
closeCameraDetailModal();
|
||
window.inspectPortal(cam.slug);
|
||
};
|
||
}
|
||
} else {
|
||
portalBox.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Admin Delete Control: ONLY visible for custom placed pins, NEVER for official scraped municipal portals
|
||
const isPlaced = cam.isCommunity || cam.id.startsWith('rpt-') || cam.id.startsWith('usr-pin-') || cam.id.startsWith('test-') || cam.id.startsWith('cam-test-');
|
||
if (el.camAdminControlBox) {
|
||
el.camAdminControlBox.style.display = isPlaced ? 'block' : 'none';
|
||
}
|
||
|
||
// Show modal
|
||
if (el.cameraDetailModal) {
|
||
if (typeof el.cameraDetailModal.showModal === 'function') {
|
||
el.cameraDetailModal.showModal();
|
||
} else {
|
||
el.cameraDetailModal.setAttribute('open', '');
|
||
}
|
||
}
|
||
}
|
||
|
||
function closeCameraDetailModal() {
|
||
if (el.cameraDetailModal) {
|
||
if (typeof el.cameraDetailModal.close === 'function') {
|
||
el.cameraDetailModal.close();
|
||
} else {
|
||
el.cameraDetailModal.removeAttribute('open');
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- ADMIN PLACED CAMERA MANAGEMENT ---
|
||
function renderAdminPlacedList() {
|
||
if (!el.adminPlacedCamsList) return;
|
||
const reports = window.FlockAuth ? window.FlockAuth.getReports() : [];
|
||
|
||
// Find all placed cameras from active radar memory (including test cameras)
|
||
const placedCams = allCameras.filter(c =>
|
||
c.isCommunity ||
|
||
c.id.startsWith('rpt-') ||
|
||
c.id.startsWith('usr-pin-') ||
|
||
c.id.startsWith('cam-test-') ||
|
||
c.id.startsWith('test-')
|
||
);
|
||
|
||
// Combine into unique map
|
||
const combinedMap = new Map();
|
||
reports.forEach(r => {
|
||
combinedMap.set(r.id, {
|
||
id: r.id,
|
||
name: r.intersection || r.type || 'Placed Camera',
|
||
lat: r.lat,
|
||
lng: r.lng,
|
||
dateStr: new Date(r.timestamp || Date.now()).toLocaleDateString(),
|
||
photoUrl: r.photoUrl || null
|
||
});
|
||
});
|
||
placedCams.forEach(c => {
|
||
if (!combinedMap.has(c.id)) {
|
||
combinedMap.set(c.id, {
|
||
id: c.id,
|
||
name: c.name || 'Test Placed Camera',
|
||
lat: c.lat,
|
||
lng: c.lng,
|
||
dateStr: 'Active Session',
|
||
photoUrl: null
|
||
});
|
||
}
|
||
});
|
||
|
||
const items = Array.from(combinedMap.values());
|
||
if (el.adminPlacedCount) el.adminPlacedCount.textContent = `${items.length} Placed`;
|
||
|
||
if (items.length === 0) {
|
||
el.adminPlacedCamsList.innerHTML = `
|
||
<div style="padding: 12px; text-align: center; color: #94a3b8; font-size: 0.8rem; background: rgba(30,41,59,0.4); border-radius: 6px;">
|
||
No custom placed cameras. All active radar cameras are official municipal portals.
|
||
</div>
|
||
`;
|
||
if (el.btnAdminClearAllPlaced) el.btnAdminClearAllPlaced.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
if (el.btnAdminClearAllPlaced) el.btnAdminClearAllPlaced.style.display = 'block';
|
||
|
||
el.adminPlacedCamsList.innerHTML = items.map(r => `
|
||
<div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 10px; background: rgba(30,41,59,0.5); border: 1px solid rgba(148,163,184,0.1); border-radius: 6px; font-size: 0.78rem;">
|
||
<div style="display: flex; align-items: center; gap: 8px; overflow: hidden; max-width: 240px;">
|
||
${r.photoUrl ? `
|
||
<img src="${r.photoUrl}" alt="Thumbnail" style="width: 34px; height: 34px; object-fit: cover; border-radius: 4px; border: 1px solid var(--accent-cyan); flex-shrink: 0;">
|
||
` : `
|
||
<span style="font-size: 1.1rem; flex-shrink: 0;">📍</span>
|
||
`}
|
||
<div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||
<strong style="color: #f8fafc; display: block; overflow: hidden; text-overflow: ellipsis;">${escapeHtml(r.name)}</strong>
|
||
<span style="color: #94a3b8; font-size: 0.7rem;">${r.lat.toFixed(4)}, ${r.lng.toFixed(4)} • ${r.dateStr}</span>
|
||
</div>
|
||
</div>
|
||
<button type="button" class="btn-secondary" style="padding: 5px 10px; font-size: 0.74rem; color: #f87171; border-color: rgba(239,68,68,0.3); cursor: pointer; flex-shrink: 0;" onclick="window.adminDeleteCamera('${escapeHtml(r.id)}')">
|
||
🗑️ Delete
|
||
</button>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
window.adminDeleteCamera = function(camId) {
|
||
// SECURITY GUARD: Protect official scraped municipal transparency portals
|
||
if (camId.startsWith('portal-') || (!camId.startsWith('rpt-') && !camId.startsWith('usr-pin-') && !camId.startsWith('test-') && !camId.startsWith('cam-test-'))) {
|
||
const targetCam = allCameras.find(c => c.id === camId);
|
||
if (targetCam && !targetCam.isCommunity) {
|
||
alert("Protected Municipal Portal: Official city and police department transparency records cannot be deleted. Only custom placed cameras can be removed.");
|
||
return;
|
||
}
|
||
}
|
||
|
||
const cam = allCameras.find(c => c.id === camId) || { name: 'Placed Camera', id: camId };
|
||
if (!confirm(`Admin Action: Permanently delete placed camera "${cam.name || cam.id}" from radar?`)) {
|
||
return;
|
||
}
|
||
|
||
if (window.FlockAuth) {
|
||
window.FlockAuth.removeReport(camId);
|
||
}
|
||
allCameras = allCameras.filter(c => c.id !== camId);
|
||
renderCameraZones();
|
||
renderAdminPlacedList();
|
||
closeCameraDetailModal();
|
||
playLogConfirmationChirp();
|
||
speakVoiceAlert("Placed camera deleted from radar.");
|
||
showToast("🗑️ Placed camera permanently removed.");
|
||
};
|
||
|
||
// --- CONTEST CAMERA MODAL ---
|
||
function openContestModal(cam) {
|
||
activeModalCamera = cam;
|
||
const reasonEl = document.getElementById('contest-reason');
|
||
if (reasonEl) reasonEl.selectedIndex = 0;
|
||
const notesEl = document.getElementById('contest-notes');
|
||
if (notesEl) notesEl.value = '';
|
||
contestPhotoPendingDataUrl = null;
|
||
if (el.contestPhotoPreviewBox) el.contestPhotoPreviewBox.classList.add('hidden');
|
||
if (el.contestPhotoBtnText) el.contestPhotoBtnText.textContent = 'Snap Empty Pole';
|
||
|
||
if (el.contestModal) {
|
||
if (typeof el.contestModal.showModal === 'function') {
|
||
el.contestModal.showModal();
|
||
} else {
|
||
el.contestModal.setAttribute('open', '');
|
||
}
|
||
}
|
||
}
|
||
|
||
function closeContestModal() {
|
||
if (el.contestModal) {
|
||
if (typeof el.contestModal.close === 'function') {
|
||
el.contestModal.close();
|
||
} else {
|
||
el.contestModal.removeAttribute('open');
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- PHOTO CAPTURE TRIGGERS ---
|
||
window.triggerPhotoCaptureForActiveCam = function() {
|
||
photoUploadTarget = 'detail';
|
||
if (el.cameraPhotoFileInput) {
|
||
el.cameraPhotoFileInput.value = '';
|
||
el.cameraPhotoFileInput.click();
|
||
}
|
||
};
|
||
|
||
function triggerPhotoCaptureForReport() {
|
||
photoUploadTarget = 'report';
|
||
if (el.cameraPhotoFileInput) {
|
||
el.cameraPhotoFileInput.value = '';
|
||
el.cameraPhotoFileInput.click();
|
||
}
|
||
}
|
||
|
||
function triggerPhotoCaptureForContest() {
|
||
photoUploadTarget = 'contest';
|
||
if (el.cameraPhotoFileInput) {
|
||
el.cameraPhotoFileInput.value = '';
|
||
el.cameraPhotoFileInput.click();
|
||
}
|
||
}
|
||
|
||
async function handlePhotoFileSelected(e) {
|
||
const file = e.target.files && e.target.files[0];
|
||
if (!file) return;
|
||
|
||
try {
|
||
showToast("📸 Processing & compressing photo proof...");
|
||
const compressedDataUrl = await window.FlockAuth.compressImage(file, 1000, 0.72);
|
||
|
||
if (photoUploadTarget === 'quicklog') {
|
||
const timeStr = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||
const newRpt = window.FlockAuth.addReport({
|
||
lat: currentPosition.lat,
|
||
lng: currentPosition.lng,
|
||
type: 'Solar Pole Mount (Falcon)',
|
||
intersection: `Driver Sighting (${timeStr})`,
|
||
notes: 'Physical photo evidence captured by scout on pole',
|
||
photoUrl: compressedDataUrl
|
||
});
|
||
|
||
loadAllCameraData();
|
||
renderCameraZones();
|
||
updateProfileDisplay();
|
||
playLogConfirmationChirp();
|
||
speakVoiceAlert("Camera photo proof verified and logged to radar grid. +150 Scout Points!");
|
||
showToast("📸 Camera photo proof verified & logged! +150 Scout Points!");
|
||
|
||
const cam = allCameras.find(c => c.id === newRpt.id);
|
||
if (cam) openCameraDetailModal(cam);
|
||
} else if (photoUploadTarget === 'detail' && activeModalCamera) {
|
||
window.FlockAuth.addPhotoProof(activeModalCamera.id, compressedDataUrl, 'Physical pole photo uploaded by scout');
|
||
openCameraDetailModal(activeModalCamera);
|
||
renderCameraZones();
|
||
playLogConfirmationChirp();
|
||
speakVoiceAlert("Photo proof verified and locked into radar database. +100 Scout Points awarded!");
|
||
showToast("📸 Photo proof verified & locked! +100 Scout Points!");
|
||
} else if (photoUploadTarget === 'report') {
|
||
reportPhotoPendingDataUrl = compressedDataUrl;
|
||
if (el.reportPhotoPreviewImg) el.reportPhotoPreviewImg.src = compressedDataUrl;
|
||
if (el.reportPhotoPreviewBox) el.reportPhotoPreviewBox.classList.remove('hidden');
|
||
if (el.reportPhotoBtnText) el.reportPhotoBtnText.textContent = 'Change Photo Proof';
|
||
showToast("📸 Photo proof attached to camera report!");
|
||
} else if (photoUploadTarget === 'contest') {
|
||
contestPhotoPendingDataUrl = compressedDataUrl;
|
||
if (el.contestPhotoPreviewImg) el.contestPhotoPreviewImg.src = compressedDataUrl;
|
||
if (el.contestPhotoPreviewBox) el.contestPhotoPreviewBox.classList.remove('hidden');
|
||
if (el.contestPhotoBtnText) el.contestPhotoBtnText.textContent = 'Change Empty Pole Photo';
|
||
showToast("📸 Empty pole photo attached to contest report!");
|
||
}
|
||
} catch (err) {
|
||
console.error('Photo processing error:', err);
|
||
alert('Could not process photo: ' + (err.message || 'Unknown error'));
|
||
}
|
||
}
|
||
|
||
window.openCameraHardwareProof = function(camId) {
|
||
const cam = allCameras.find(c => c.id === camId);
|
||
if (cam) openCameraDetailModal(cam);
|
||
};
|
||
|
||
// --- EVENT BINDINGS ---
|
||
function bindEvents() {
|
||
// Live GPS Continuous Tracking
|
||
el.btnLiveGps.addEventListener('click', toggleLiveGps);
|
||
|
||
// Map Layer Switcher (Dark Canvas / Satellite)
|
||
if (el.btnMapLayer) {
|
||
el.btnMapLayer.addEventListener('click', toggleMapLayer);
|
||
}
|
||
|
||
// Quick Location Buttons
|
||
if (el.btnLocateMe) {
|
||
el.btnLocateMe.addEventListener('click', useLiveGps);
|
||
}
|
||
if (el.btnLocPalmdale) {
|
||
el.btnLocPalmdale.addEventListener('click', () => {
|
||
jumpToLocation(34.60194, -118.14847, 'Palmdale Blvd & 10th St W');
|
||
});
|
||
}
|
||
if (el.btnLocPalmdaleEast) {
|
||
el.btnLocPalmdaleEast.addEventListener('click', () => {
|
||
jumpToLocation(34.58081, -118.04497, 'East Palmdale (Ave R & 47th St E)');
|
||
});
|
||
}
|
||
if (el.btnLocPalmdaleSouth) {
|
||
el.btnLocPalmdaleSouth.addEventListener('click', () => {
|
||
jumpToLocation(34.56829, -118.14702, 'South Palmdale (Ave S & 10th St W)');
|
||
});
|
||
}
|
||
if (el.btnLocLancaster) {
|
||
el.btnLocLancaster.addEventListener('click', () => {
|
||
jumpToLocation(34.69718, -118.14770, 'Lancaster (Ave L & 10th St W)');
|
||
});
|
||
}
|
||
if (el.btnLocRancho) {
|
||
el.btnLocRancho.addEventListener('click', () => {
|
||
jumpToLocation(34.63179, -118.12594, 'West AV (Rancho Vista & AV Mall)');
|
||
});
|
||
}
|
||
|
||
// Address & City Search
|
||
if (el.mapSearchForm) {
|
||
el.mapSearchForm.addEventListener('submit', e => {
|
||
e.preventDefault();
|
||
const query = el.mapSearchInput.value.trim();
|
||
if (query) searchLocation(query);
|
||
});
|
||
}
|
||
|
||
el.btnTestCamera.addEventListener('click', placeTestCameraAhead);
|
||
el.btnTestAlert.addEventListener('click', testAlertSoundAndVoice);
|
||
el.btnSimDrive.addEventListener('click', toggleDriveSimulation);
|
||
el.btnResetSim.addEventListener('click', resetDriveSimulation);
|
||
el.btnMobileConnect.addEventListener('click', showMobileConnectModal);
|
||
|
||
// New 5% Driving Enhancement Listeners
|
||
if (el.btnFollowCar) el.btnFollowCar.addEventListener('click', toggleFollowCar);
|
||
if (el.btnRecenterCar) el.btnRecenterCar.addEventListener('click', recenterOnCar);
|
||
if (el.btnQuickLogCam) el.btnQuickLogCam.addEventListener('click', quickLogFlockCameraAtCurrentLocation);
|
||
if (el.btnFullscreenMount) el.btnFullscreenMount.addEventListener('click', toggleFullscreenMount);
|
||
if (el.btnSensCity) el.btnSensCity.addEventListener('click', () => setRadarSensitivity('city'));
|
||
if (el.btnSensHwy) el.btnSensHwy.addEventListener('click', () => setRadarSensitivity('highway'));
|
||
|
||
// Camera Hardware Proof & Voting Events
|
||
if (el.camDetailCloseBtn) el.camDetailCloseBtn.addEventListener('click', closeCameraDetailModal);
|
||
|
||
if (el.btnVoteConfirmCam) {
|
||
el.btnVoteConfirmCam.addEventListener('click', () => {
|
||
if (!activeModalCamera) return;
|
||
const res = window.FlockAuth.confirmCamera(activeModalCamera.id);
|
||
if (res.success) {
|
||
playLogConfirmationChirp();
|
||
speakVoiceAlert("Camera confirmed active on pole. +25 Scout Points!");
|
||
showToast(`👍 Confirmed active on pole! Total: ${res.count} confirmations.`);
|
||
openCameraDetailModal(activeModalCamera);
|
||
renderCameraZones();
|
||
updateProfileDisplay();
|
||
} else {
|
||
showToast(res.message || "Already confirmed!");
|
||
}
|
||
});
|
||
}
|
||
|
||
if (el.btnProofAddPhoto) {
|
||
el.btnProofAddPhoto.addEventListener('click', () => {
|
||
window.triggerPhotoCaptureForActiveCam();
|
||
});
|
||
}
|
||
|
||
if (el.btnProofContest) {
|
||
el.btnProofContest.addEventListener('click', () => {
|
||
if (!activeModalCamera) return;
|
||
closeCameraDetailModal();
|
||
openContestModal(activeModalCamera);
|
||
});
|
||
}
|
||
|
||
// Admin Delete Camera Button (Detail Modal)
|
||
if (el.btnAdminDeleteCam) {
|
||
el.btnAdminDeleteCam.addEventListener('click', () => {
|
||
if (!activeModalCamera) return;
|
||
window.adminDeleteCamera(activeModalCamera.id);
|
||
});
|
||
}
|
||
|
||
// Contest Modal Events
|
||
if (el.contestCloseBtn) el.contestCloseBtn.addEventListener('click', closeContestModal);
|
||
if (el.btnContestAddPhoto) el.btnContestAddPhoto.addEventListener('click', triggerPhotoCaptureForContest);
|
||
if (el.btnContestRemovePhoto) {
|
||
el.btnContestRemovePhoto.addEventListener('click', () => {
|
||
contestPhotoPendingDataUrl = null;
|
||
if (el.contestPhotoPreviewBox) el.contestPhotoPreviewBox.classList.add('hidden');
|
||
if (el.contestPhotoBtnText) el.contestPhotoBtnText.textContent = 'Snap Empty Pole';
|
||
});
|
||
}
|
||
|
||
if (el.contestForm) {
|
||
el.contestForm.addEventListener('submit', e => {
|
||
e.preventDefault();
|
||
if (!activeModalCamera) return;
|
||
const reason = document.getElementById('contest-reason').value;
|
||
const notes = document.getElementById('contest-notes').value.trim();
|
||
|
||
const res = window.FlockAuth.contestCamera(activeModalCamera.id, reason, notes);
|
||
closeContestModal();
|
||
|
||
if (res.success) {
|
||
if (res.isContested) {
|
||
speakVoiceAlert("Camera flagged as contested by community consensus.");
|
||
showToast("⚠️ Location marked Contested by community consensus.");
|
||
} else {
|
||
showToast(`⚠️ Contest submitted (${res.contestCount}/3 flags for consensus review).`);
|
||
}
|
||
renderCameraZones();
|
||
} else {
|
||
showToast(res.message || "Already reported!");
|
||
}
|
||
});
|
||
}
|
||
|
||
// Report Form Photo Proof Events
|
||
if (el.btnReportAddPhoto) el.btnReportAddPhoto.addEventListener('click', triggerPhotoCaptureForReport);
|
||
if (el.btnReportRemovePhoto) {
|
||
el.btnReportRemovePhoto.addEventListener('click', () => {
|
||
reportPhotoPendingDataUrl = null;
|
||
if (el.reportPhotoPreviewBox) el.reportPhotoPreviewBox.classList.add('hidden');
|
||
if (el.reportPhotoBtnText) el.reportPhotoBtnText.textContent = 'Snap Camera on Pole (+100 pts)';
|
||
});
|
||
}
|
||
|
||
// Native Camera / File Input Change Event
|
||
if (el.cameraPhotoFileInput) {
|
||
el.cameraPhotoFileInput.addEventListener('change', handlePhotoFileSelected);
|
||
}
|
||
|
||
// Nearest Camera HUD tap opens details
|
||
const hudLocationBar = document.querySelector('.hud-location-bar');
|
||
if (hudLocationBar) {
|
||
hudLocationBar.style.cursor = 'pointer';
|
||
hudLocationBar.title = 'Click to inspect nearest camera hardware proof & vote';
|
||
hudLocationBar.addEventListener('click', () => {
|
||
if (allCameras.length > 0) {
|
||
let closest = null;
|
||
let minDist = Infinity;
|
||
allCameras.forEach(cam => {
|
||
const d = getHaversineDistanceMeters(currentPosition.lat, currentPosition.lng, cam.lat, cam.lng);
|
||
if (d < minDist) { minDist = d; closest = cam; }
|
||
});
|
||
if (closest) openCameraDetailModal(closest);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Mute toggle
|
||
el.btnMuteSound.addEventListener('click', () => {
|
||
const user = window.FlockAuth ? window.FlockAuth.getUser() : null;
|
||
if (user) {
|
||
user.soundEnabled = !user.soundEnabled;
|
||
window.FlockAuth.saveUser();
|
||
el.btnMuteSound.innerHTML = user.soundEnabled ? '🔔 Sound: ON' : '🔕 Sound: OFF';
|
||
el.btnMuteSound.classList.toggle('active', !user.soundEnabled);
|
||
}
|
||
});
|
||
|
||
// Report Camera FAB
|
||
if (el.btnReportCameraFab) el.btnReportCameraFab.addEventListener('click', openReportModal);
|
||
|
||
// Profile button click
|
||
el.driverProfileBtn.addEventListener('click', openLoginModal);
|
||
|
||
// Login Form Submit
|
||
el.loginForm.addEventListener('submit', e => {
|
||
e.preventDefault();
|
||
const username = document.getElementById('login-username').value.trim();
|
||
const vehicle = document.getElementById('login-vehicle').value.trim();
|
||
const soundEnabled = document.getElementById('login-sound').checked;
|
||
|
||
if (window.FlockAuth) {
|
||
window.FlockAuth.login(username, vehicle, soundEnabled);
|
||
updateProfileDisplay();
|
||
}
|
||
closeLoginModal();
|
||
});
|
||
|
||
// Login as Guest button
|
||
el.loginGuestBtn.addEventListener('click', () => {
|
||
if (window.FlockAuth) {
|
||
window.FlockAuth.login('Guest Scout', 'Unspecified Vehicle', true);
|
||
updateProfileDisplay();
|
||
}
|
||
closeLoginModal();
|
||
});
|
||
|
||
// Admin Clear All Placed Pins Button (Keeps 1,439 real municipal portals)
|
||
if (el.btnAdminClearAllPlaced) {
|
||
el.btnAdminClearAllPlaced.addEventListener('click', () => {
|
||
if (!confirm("Admin Action: Clear ALL custom placed pins and radar test markers? (The 1,439 official municipal portals will remain permanent and protected).")) {
|
||
return;
|
||
}
|
||
if (window.FlockAuth) {
|
||
window.FlockAuth.clearAllPlacedReports();
|
||
}
|
||
allCameras = allCameras.filter(c => !c.isCommunity && !c.id.startsWith('rpt-') && !c.id.startsWith('usr-pin-') && !c.id.startsWith('test-') && !c.id.startsWith('cam-test-'));
|
||
renderCameraZones();
|
||
renderAdminPlacedList();
|
||
playLogConfirmationChirp();
|
||
speakVoiceAlert("All custom placed pins cleared from radar.");
|
||
showToast("🗑️ All custom placed pins cleared.");
|
||
});
|
||
}
|
||
|
||
// Admin Placed Pins Shortcut in Map Top Controls
|
||
if (el.btnAdminManagePins) {
|
||
el.btnAdminManagePins.addEventListener('click', () => {
|
||
openLoginModal();
|
||
setTimeout(() => {
|
||
const adminSec = document.querySelector('.admin-profile-section');
|
||
if (adminSec) adminSec.scrollIntoView({ behavior: 'smooth' });
|
||
}, 120);
|
||
});
|
||
}
|
||
|
||
// Sync radar when reports updated
|
||
window.addEventListener('flock_reports_updated', () => {
|
||
loadAllCameraData();
|
||
renderCameraZones();
|
||
renderAdminPlacedList();
|
||
});
|
||
|
||
// Report Form Submit
|
||
el.reportForm.addEventListener('submit', e => {
|
||
e.preventDefault();
|
||
const type = document.getElementById('report-mount-type').value;
|
||
const intersection = document.getElementById('report-intersection').value.trim();
|
||
const notes = document.getElementById('report-notes').value.trim();
|
||
const lat = parseFloat(document.getElementById('report-lat').value);
|
||
const lng = parseFloat(document.getElementById('report-lng').value);
|
||
|
||
if (window.FlockAuth) {
|
||
const newRpt = window.FlockAuth.addReport({
|
||
lat,
|
||
lng,
|
||
type,
|
||
intersection,
|
||
notes,
|
||
photoUrl: reportPhotoPendingDataUrl
|
||
});
|
||
|
||
reportPhotoPendingDataUrl = null;
|
||
if (el.reportPhotoPreviewBox) el.reportPhotoPreviewBox.classList.add('hidden');
|
||
if (el.reportPhotoBtnText) el.reportPhotoBtnText.textContent = 'Snap Camera on Pole (+100 pts)';
|
||
|
||
loadAllCameraData();
|
||
renderCameraZones();
|
||
updateProfileDisplay();
|
||
playLogConfirmationChirp();
|
||
speakVoiceAlert("Camera sighting broadcast to radar network!");
|
||
showToast(`Camera report added! +${newRpt.photoUrl ? '150' : '50'} Scout Points.`);
|
||
}
|
||
|
||
closeReportModal();
|
||
});
|
||
|
||
// Modal Close buttons
|
||
el.loginCloseBtn.addEventListener('click', closeLoginModal);
|
||
el.reportCloseBtn.addEventListener('click', closeReportModal);
|
||
|
||
// Global listener for portal inspection from popup
|
||
window.inspectPortal = function(slug) {
|
||
switchView('dir');
|
||
setTimeout(() => {
|
||
window.location.hash = slug;
|
||
}, 150);
|
||
};
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
if (!str) return '';
|
||
return String(str)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
// --- PWA SERVICE WORKER & MOBILE WAKE LOCK ---
|
||
if ('serviceWorker' in navigator) {
|
||
window.addEventListener('load', () => {
|
||
navigator.serviceWorker.register('./sw.js').catch(err => {
|
||
console.log('Service Worker registration skipped/failed:', err);
|
||
});
|
||
});
|
||
}
|
||
|
||
// Screen Wake Lock to prevent phone screen from going to sleep while driving
|
||
let wakeLock = null;
|
||
async function requestWakeLock() {
|
||
if ('wakeLock' in navigator) {
|
||
try {
|
||
wakeLock = await navigator.wakeLock.request('screen');
|
||
} catch (err) {
|
||
console.warn('Wake Lock error:', err);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Auto-unlock Web Audio and Wake Lock on first touch (critical for mobile iOS/Android)
|
||
function unlockMobileFeatures() {
|
||
if (!audioContext) {
|
||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||
}
|
||
if (audioContext && audioContext.state === 'suspended') {
|
||
audioContext.resume();
|
||
}
|
||
requestWakeLock();
|
||
document.removeEventListener('touchstart', unlockMobileFeatures);
|
||
document.removeEventListener('click', unlockMobileFeatures);
|
||
}
|
||
document.addEventListener('touchstart', unlockMobileFeatures, { passive: true });
|
||
document.addEventListener('click', unlockMobileFeatures, { passive: true });
|
||
|
||
// --- START ENGINE ON DOM READY ---
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
|
||
})();
|