feat(v27): directional FOV approach filter, dynamic ETA countdown, windshield mirror HUD, stealth AMOLED night mode, 1-tap driving sighting stamp, audio boost and steering haptics
This commit is contained in:
+266
-17
@@ -489,15 +489,15 @@ body {
|
||||
/* HUD Metrics */
|
||||
.hud-metrics-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.hud-metric-box {
|
||||
background: rgba(7, 9, 14, 0.6);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 12px;
|
||||
padding: 10px 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -2948,7 +2948,7 @@ body {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Compact Floating Action Button on Mobile */
|
||||
/* Compact Floating Action Buttons on Mobile */
|
||||
.driver-fab-container {
|
||||
position: absolute;
|
||||
bottom: max(18px, env(safe-area-inset-bottom));
|
||||
@@ -2956,22 +2956,37 @@ body {
|
||||
z-index: 500;
|
||||
margin: 0;
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-driver-fab {
|
||||
padding: 10px 16px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 9999px;
|
||||
gap: 8px;
|
||||
box-shadow: 0 4px 16px rgba(239, 68, 68, 0.45);
|
||||
gap: 6px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.btn-fab-stamp {
|
||||
background: linear-gradient(135deg, #0284c7, #0369a1);
|
||||
border: 1.5px solid rgba(56, 189, 248, 0.5);
|
||||
box-shadow: 0 4px 16px rgba(2, 132, 199, 0.45);
|
||||
}
|
||||
|
||||
.btn-fab-photo {
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
border: 1.5px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 4px 16px rgba(239, 68, 68, 0.45);
|
||||
}
|
||||
|
||||
.fab-icon-glow {
|
||||
font-size: 1.25rem;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.fab-main-title {
|
||||
font-size: 0.84rem;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -3277,37 +3292,56 @@ body {
|
||||
box-shadow: 0 0 8px rgba(56, 189, 248, 0.3);
|
||||
}
|
||||
|
||||
/* 3. Giant 1-Tap "Log Flock Camera" Driver FAB */
|
||||
/* 3. Floating Action Buttons: 1-Tap Sighting Stamp & Photo Log */
|
||||
.driver-fab-container {
|
||||
position: absolute;
|
||||
bottom: 25px;
|
||||
right: 25px;
|
||||
z-index: 1100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.btn-driver-fab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: linear-gradient(135deg, #ef4444 0%, #b91c1c 100%);
|
||||
color: #ffffff;
|
||||
border: 2px solid rgba(255, 255, 255, 0.4);
|
||||
border-radius: 50px;
|
||||
padding: 14px 22px;
|
||||
font-size: 0.95rem;
|
||||
padding: 12px 20px;
|
||||
font-size: 0.92rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 6px 25px rgba(239, 68, 68, 0.55), 0 0 15px rgba(239, 68, 68, 0.4);
|
||||
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
user-select: none;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.btn-driver-fab:hover {
|
||||
.btn-fab-stamp {
|
||||
background: linear-gradient(135deg, #0284c7 0%, #0369a1 100%);
|
||||
border: 2px solid rgba(56, 189, 248, 0.6);
|
||||
box-shadow: 0 6px 25px rgba(2, 132, 199, 0.5), 0 0 15px rgba(56, 189, 248, 0.35);
|
||||
}
|
||||
|
||||
.btn-fab-stamp:hover {
|
||||
transform: translateY(-2px) scale(1.02);
|
||||
border-color: #38bdf8;
|
||||
box-shadow: 0 8px 30px rgba(2, 132, 199, 0.7), 0 0 22px rgba(56, 189, 248, 0.5);
|
||||
}
|
||||
|
||||
.btn-fab-photo {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #b91c1c 100%);
|
||||
border: 2px solid rgba(255, 255, 255, 0.4);
|
||||
box-shadow: 0 6px 25px rgba(239, 68, 68, 0.55), 0 0 15px rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
.btn-fab-photo:hover {
|
||||
transform: translateY(-2px) scale(1.02);
|
||||
box-shadow: 0 8px 30px rgba(239, 68, 68, 0.75), 0 0 25px rgba(239, 68, 68, 0.5);
|
||||
border-color: #ffffff;
|
||||
box-shadow: 0 8px 30px rgba(239, 68, 68, 0.75), 0 0 25px rgba(239, 68, 68, 0.5);
|
||||
}
|
||||
|
||||
.btn-driver-fab:active {
|
||||
@@ -4689,3 +4723,218 @@ body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
v27 DRIVER-CENTRIC POLISH REFINEMENTS
|
||||
1. Directional Approach Angle & FOV Badges
|
||||
2. Time-to-Zone Intercept Dynamic Countdown
|
||||
3. Windshield Reflection Mirror Mode & Stealth AMOLED Mode
|
||||
4. Instant 1-Tap Driving Sighting Stamp
|
||||
5. Tools Drawer Cockpit HUD & Sound/Haptics Controls
|
||||
========================================================================== */
|
||||
|
||||
/* Directional Approach Angle Badges */
|
||||
.hud-approach-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 0.66rem;
|
||||
font-weight: 700;
|
||||
padding: 3px 8px;
|
||||
border-radius: 9999px;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
transition: all 0.25s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hud-approach-badge.approach-direct {
|
||||
background: rgba(239, 68, 68, 0.22);
|
||||
color: #f87171;
|
||||
border: 1px solid rgba(239, 68, 68, 0.5);
|
||||
box-shadow: 0 0 10px rgba(239, 68, 68, 0.35);
|
||||
animation: pulse-approach 1.2s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes pulse-approach {
|
||||
0% { transform: scale(1); box-shadow: 0 0 6px rgba(239, 68, 68, 0.3); }
|
||||
100% { transform: scale(1.04); box-shadow: 0 0 12px rgba(239, 68, 68, 0.6); }
|
||||
}
|
||||
|
||||
.hud-approach-badge.approach-cross {
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
color: #fbbf24;
|
||||
border: 1px solid rgba(245, 158, 11, 0.45);
|
||||
}
|
||||
|
||||
.hud-approach-badge.approach-away {
|
||||
background: rgba(148, 163, 184, 0.15);
|
||||
color: #94a3b8;
|
||||
border: 1px solid rgba(148, 163, 184, 0.3);
|
||||
}
|
||||
|
||||
.hud-approach-badge.approach-none {
|
||||
background: rgba(16, 185, 129, 0.14);
|
||||
color: #34d399;
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
/* Time-to-Zone Intercept Countdown */
|
||||
#hud-eta-box {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
#hud-eta-box .hud-metric-number {
|
||||
color: #38bdf8;
|
||||
text-shadow: 0 0 8px rgba(56, 189, 248, 0.3);
|
||||
}
|
||||
|
||||
/* Windshield Reflection Mirror Mode (Inverts horizontally for dashboard reflection) */
|
||||
.mirror-hud {
|
||||
transform: scaleX(-1) !important;
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
/* Stealth AMOLED Pitch Dark Mode (#000000 Zero Cockpit Glare) */
|
||||
body.stealth-amoled {
|
||||
background-color: #000000 !important;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.stealth-amoled .radar-view-container,
|
||||
body.stealth-amoled .radar-map-canvas,
|
||||
body.stealth-amoled #radar-map {
|
||||
background-color: #000000 !important;
|
||||
filter: brightness(0.4) contrast(1.5) saturate(0.2) !important;
|
||||
}
|
||||
|
||||
body.stealth-amoled .driver-hud {
|
||||
background: #000000 !important;
|
||||
border-color: #1e293b !important;
|
||||
box-shadow: 0 0 24px rgba(0, 0, 0, 0.95), 0 0 0 1px #111827 !important;
|
||||
backdrop-filter: none !important;
|
||||
-webkit-backdrop-filter: none !important;
|
||||
}
|
||||
|
||||
body.stealth-amoled .hud-capsule-header {
|
||||
background: #000000 !important;
|
||||
border-color: #1e293b !important;
|
||||
}
|
||||
|
||||
body.stealth-amoled .hud-metric-box {
|
||||
background: #040608 !important;
|
||||
border-color: #1e293b !important;
|
||||
}
|
||||
|
||||
body.stealth-amoled .hud-metric-number {
|
||||
color: #00f0ff !important;
|
||||
text-shadow: 0 0 8px rgba(0, 240, 255, 0.45) !important;
|
||||
}
|
||||
|
||||
body.stealth-amoled .map-top-controls {
|
||||
background: rgba(0, 0, 0, 0.98) !important;
|
||||
border-color: #1e293b !important;
|
||||
}
|
||||
|
||||
body.stealth-amoled .app-header,
|
||||
body.stealth-amoled .site-nav {
|
||||
background: #000000 !important;
|
||||
border-color: #1e293b !important;
|
||||
}
|
||||
|
||||
/* Tools Drawer Toggle Grid */
|
||||
.tools-toggle-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.tools-toggle-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Saved Sighting Stamps in Tools Drawer */
|
||||
.stamps-list-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
max-height: 140px;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.stamps-empty-hint {
|
||||
font-size: 0.76rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.stamp-item-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
font-size: 0.74rem;
|
||||
color: #cbd5e1;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.stamp-item-card:hover {
|
||||
background: rgba(56, 189, 248, 0.1);
|
||||
border-color: rgba(56, 189, 248, 0.3);
|
||||
}
|
||||
|
||||
.stamp-item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.stamp-badge-zap {
|
||||
color: #f59e0b;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn-mini-clear {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
font-size: 0.72rem;
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.btn-mini-clear:hover {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Temporary Stamp Marker Animation on Map */
|
||||
.stamp-pulse-pin {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f59e0b;
|
||||
color: #000;
|
||||
font-weight: 800;
|
||||
font-size: 14px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 0 16px rgba(245, 158, 11, 0.85);
|
||||
animation: stamp-drop 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
|
||||
@keyframes stamp-drop {
|
||||
0% { transform: scale(0.3) translateY(-20px); opacity: 0; }
|
||||
70% { transform: scale(1.2) translateY(3px); opacity: 1; }
|
||||
100% { transform: scale(1) translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
+77
-15
@@ -54,7 +54,7 @@
|
||||
<meta name="apple-mobile-web-app-title" content="FlockRadar">
|
||||
|
||||
<!-- Main Stylesheet -->
|
||||
<link rel="stylesheet" href="css/styles.css?v=26">
|
||||
<link rel="stylesheet" href="css/styles.css?v=27">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
<span>ALL CLEAR</span>
|
||||
</div>
|
||||
|
||||
<!-- Metrics Row: Speed + Nearest Distance -->
|
||||
<!-- Metrics Row: Speed + Nearest Distance + Intercept ETA -->
|
||||
<div class="hud-metrics-row">
|
||||
<div class="hud-metric-box">
|
||||
<div class="hud-val-group">
|
||||
@@ -175,6 +175,14 @@
|
||||
</div>
|
||||
<span class="hud-metric-label">Nearest</span>
|
||||
</div>
|
||||
|
||||
<div class="hud-metric-box" id="hud-eta-box">
|
||||
<div class="hud-val-group">
|
||||
<span class="hud-metric-number" id="hud-eta-val">—</span>
|
||||
<span class="hud-metric-unit" id="hud-eta-sub">SEC</span>
|
||||
</div>
|
||||
<span class="hud-metric-label">Intercept</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header Quick Triggers (Audio Toggle, Tools, Search) -->
|
||||
@@ -197,14 +205,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Agency / Street Monitoring -->
|
||||
<!-- Current Agency / Street Monitoring & Approach Vector Badge -->
|
||||
<div class="hud-location-bar">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--accent-cyan)" stroke-width="2">
|
||||
<div class="hud-loc-left" style="display:flex; align-items:center; gap:6px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:1; min-width:0;">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--accent-cyan)" stroke-width="2" style="flex-shrink:0;">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
||||
<polyline points="11 12 12 12 12 16 13 16"></polyline>
|
||||
</svg>
|
||||
<span id="hud-cam-agency">Monitoring Roadway Grid</span>
|
||||
<span id="hud-cam-agency" style="overflow:hidden; text-overflow:ellipsis;">Monitoring Roadway Grid</span>
|
||||
</div>
|
||||
<span id="hud-approach-badge" class="hud-approach-badge approach-none">ALL CLEAR</span>
|
||||
</div>
|
||||
|
||||
<!-- High-Priority Pulsing Warning Banner -->
|
||||
@@ -370,6 +381,47 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Cockpit Display & Windshield Reflection HUD -->
|
||||
<div class="tools-group">
|
||||
<div class="tools-group-label">COCKPIT & WINDSHIELD HUD</div>
|
||||
<div class="tools-toggle-grid">
|
||||
<button type="button" id="btn-mirror-hud" class="btn-hud-action btn-tool-item" title="Mirror HUD horizontally so phone placed flat on dashboard reflects right-side up on windshield glass">
|
||||
<svg class="flock-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="7 8 3 12 7 16"/><polyline points="17 8 21 12 17 16"/><line x1="14" y1="4" x2="10" y2="20"/></svg>
|
||||
<span id="mirror-hud-label">Windshield Mirror: OFF</span>
|
||||
</button>
|
||||
<button type="button" id="btn-stealth-amoled" class="btn-hud-action btn-tool-item" title="Switch to pitch-black #000000 AMOLED stealth mode for zero cockpit glare">
|
||||
<svg class="flock-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
||||
<span id="stealth-amoled-label">AMOLED Pitch Dark: OFF</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cabin Sound Boost & Haptic Feedback -->
|
||||
<div class="tools-group">
|
||||
<div class="tools-group-label">CABIN SOUND & HAPTICS</div>
|
||||
<div class="tools-toggle-grid">
|
||||
<button type="button" id="btn-toggle-audio-boost" class="btn-hud-action btn-tool-item" title="Boost radar siren & voice chime volume by +6dB (160%) for loud road noise">
|
||||
<svg class="flock-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/><line x1="22" y1="2" x2="22" y2="8"/><line x1="19" y1="5" x2="25" y2="5"/></svg>
|
||||
<span id="audio-boost-label">Audio Boost (+6dB): OFF</span>
|
||||
</button>
|
||||
<button type="button" id="btn-toggle-haptic-boost" class="btn-hud-action btn-tool-item active" title="Vibrate phone on zone entry like lane-keep steering wheel feedback">
|
||||
<svg class="flock-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="2" y1="8" x2="2" y2="16"/><line x1="22" y1="8" x2="22" y2="16"/></svg>
|
||||
<span id="haptic-boost-label">Steering Haptics: ON</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Instant Stamped Sightings -->
|
||||
<div class="tools-group" id="group-quick-stamps">
|
||||
<div class="tools-group-label" style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>SAVED SIGHTINGS (<span id="stamps-count">0</span>)</span>
|
||||
<button type="button" id="btn-clear-stamps" class="btn-mini-clear" title="Clear all local quick stamps">Clear</button>
|
||||
</div>
|
||||
<div id="stamps-list-container" class="stamps-list-container">
|
||||
<p class="stamps-empty-hint">No quick stamps yet. Tap ⚡ Stamp while driving to record GPS instantly.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Driving Audio Alert Mode Selector -->
|
||||
<div class="tools-group">
|
||||
<div class="tools-group-label">AUDIO ALERT STYLE</div>
|
||||
@@ -506,15 +558,25 @@
|
||||
<span>Recenter on Vehicle</span>
|
||||
</button>
|
||||
|
||||
<!-- Giant 1-Tap Floating Action Button: Snap & Log Flock Camera (Bottom-Right) -->
|
||||
<!-- Floating Action Buttons: Instant GPS Sighting Stamp + Photo Evidence (Bottom-Right) -->
|
||||
<div class="driver-fab-container">
|
||||
<button id="btn-quick-log-cam" class="btn-driver-fab" title="Snap camera photo on pole & log at GPS location">
|
||||
<button id="btn-quick-mark-dash" class="btn-driver-fab btn-fab-stamp" title="⚡ 1-Tap Instant Sighting Stamp (Safe while driving - records GPS location instantly without typing)">
|
||||
<div class="fab-icon-glow">
|
||||
<svg class="flock-svg flock-svg-lg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/></svg>
|
||||
<svg class="flock-svg flock-svg-md" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
||||
</div>
|
||||
<div class="fab-text-group">
|
||||
<span class="fab-main-title">+ SNAP & LOG CAMERA</span>
|
||||
<span class="fab-sub-title">Opens Camera for Photo Proof</span>
|
||||
<span class="fab-main-title">⚡ STAMP</span>
|
||||
<span class="fab-sub-title">1-Tap GPS Mark</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button id="btn-quick-log-cam" class="btn-driver-fab btn-fab-photo" title="Snap camera photo on pole & log at GPS location">
|
||||
<div class="fab-icon-glow">
|
||||
<svg class="flock-svg flock-svg-md" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/></svg>
|
||||
</div>
|
||||
<div class="fab-text-group">
|
||||
<span class="fab-main-title">+ SNAP</span>
|
||||
<span class="fab-sub-title">Photo Proof</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1509,11 +1571,11 @@
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
|
||||
<!-- Data & Application Logic with Cache Busting -->
|
||||
<script src="data/portals.js?v=26"></script>
|
||||
<script src="js/icons.js?v=26"></script>
|
||||
<script src="js/auth.js?v=26"></script>
|
||||
<script src="js/radar.js?v=26"></script>
|
||||
<script src="js/app.js?v=26"></script>
|
||||
<script src="data/portals.js?v=27"></script>
|
||||
<script src="js/icons.js?v=27"></script>
|
||||
<script src="js/auth.js?v=27"></script>
|
||||
<script src="js/radar.js?v=27"></script>
|
||||
<script src="js/app.js?v=27"></script>
|
||||
|
||||
<!-- Service Worker Instant Cache Invalidation -->
|
||||
<script>
|
||||
|
||||
+493
-70
@@ -112,6 +112,15 @@
|
||||
let bgAudioElement = null;
|
||||
let activeDetourLayerGroup = null;
|
||||
|
||||
// --- V27 DRIVER-CENTRIC POLISH STATE ---
|
||||
let isHudMirrored = localStorage.getItem('flock_hud_mirror') === 'true';
|
||||
let isStealthAmoled = localStorage.getItem('flock_stealth_amoled') === 'true';
|
||||
let isAudioBoostEnabled = localStorage.getItem('flock_radar_audio_boost') === 'true';
|
||||
let isHapticBoostEnabled = localStorage.getItem('flock_radar_haptic_boost') !== 'false';
|
||||
let masterGainNode = null;
|
||||
let savedQuickStamps = [];
|
||||
let stampMarkers = [];
|
||||
|
||||
const SURVEILLANCE_CORRIDORS = {
|
||||
'palmdale-blvd': {
|
||||
name: 'Palmdale Blvd Corridor (SR-14 to 50th St E)',
|
||||
@@ -406,7 +415,25 @@
|
||||
btnGeneratePra: document.getElementById('btn-generate-pra'),
|
||||
btnCopyPraLetter: document.getElementById('btn-copy-pra-letter'),
|
||||
btnEmailPraLetter: document.getElementById('btn-email-pra-letter'),
|
||||
praLetterText: document.getElementById('pra-letter-text')
|
||||
praLetterText: document.getElementById('pra-letter-text'),
|
||||
|
||||
// Cockpit, FOV & Driver Polish (v27)
|
||||
hudApproachBadge: document.getElementById('hud-approach-badge'),
|
||||
hudEtaBox: document.getElementById('hud-eta-box'),
|
||||
hudEtaVal: document.getElementById('hud-eta-val'),
|
||||
hudEtaSub: document.getElementById('hud-eta-sub'),
|
||||
btnQuickMarkDash: document.getElementById('btn-quick-mark-dash'),
|
||||
btnMirrorHud: document.getElementById('btn-mirror-hud'),
|
||||
mirrorHudLabel: document.getElementById('mirror-hud-label'),
|
||||
btnStealthAmoled: document.getElementById('btn-stealth-amoled'),
|
||||
stealthAmoledLabel: document.getElementById('stealth-amoled-label'),
|
||||
btnToggleAudioBoost: document.getElementById('btn-toggle-audio-boost'),
|
||||
audioBoostLabel: document.getElementById('audio-boost-label'),
|
||||
btnToggleHapticBoost: document.getElementById('btn-toggle-haptic-boost'),
|
||||
hapticBoostLabel: document.getElementById('haptic-boost-label'),
|
||||
stampsCount: document.getElementById('stamps-count'),
|
||||
stampsListContainer: document.getElementById('stamps-list-container'),
|
||||
btnClearStamps: document.getElementById('btn-clear-stamps')
|
||||
};
|
||||
|
||||
// --- INITIALIZATION ---
|
||||
@@ -421,6 +448,13 @@
|
||||
updateProfileDisplay();
|
||||
checkSupporterReturn();
|
||||
|
||||
// Initialize v27 Driver Cockpit Preferences & Quick Stamps
|
||||
applyHudMirror(isHudMirrored, false);
|
||||
applyStealthAmoled(isStealthAmoled, false);
|
||||
applyAudioBoost(isAudioBoostEnabled, false);
|
||||
applyHapticBoost(isHapticBoostEnabled, false);
|
||||
loadQuickStamps();
|
||||
|
||||
// Auto-detect real location immediately (Palmdale / GPS)
|
||||
attemptAutoLocate();
|
||||
|
||||
@@ -780,6 +814,12 @@
|
||||
function setupHud() {
|
||||
updateHudSpeedAndHeading(0, null);
|
||||
el.hudDistVal.textContent = '—';
|
||||
if (el.hudEtaVal) el.hudEtaVal.textContent = '—';
|
||||
if (el.hudEtaSub) el.hudEtaSub.textContent = 'IDLE';
|
||||
if (el.hudApproachBadge) {
|
||||
el.hudApproachBadge.className = 'hud-approach-badge approach-none';
|
||||
el.hudApproachBadge.textContent = 'ALL CLEAR';
|
||||
}
|
||||
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';
|
||||
@@ -788,6 +828,38 @@
|
||||
updateVehicleSupporterAura();
|
||||
}
|
||||
|
||||
// --- DIRECTIONAL TRAVEL FILTER & APPROACH ANGLE CLASSIFIER (v27) ---
|
||||
function getBearing(lat1, lng1, lat2, lng2) {
|
||||
const toRad = deg => deg * Math.PI / 180;
|
||||
const toDeg = rad => rad * 180 / Math.PI;
|
||||
const dLng = toRad(lng2 - lng1);
|
||||
const lat1Rad = toRad(lat1);
|
||||
const lat2Rad = toRad(lat2);
|
||||
const y = Math.sin(dLng) * Math.cos(lat2Rad);
|
||||
const x = Math.cos(lat1Rad) * Math.sin(lat2Rad) -
|
||||
Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(dLng);
|
||||
return (toDeg(Math.atan2(y, x)) + 360) % 360;
|
||||
}
|
||||
|
||||
function classifyApproach(userLat, userLng, userHeading, camLat, camLng, speedMph) {
|
||||
if (speedMph < 3 || userHeading === null || isNaN(userHeading)) {
|
||||
return { type: 'proximity', label: 'RADAR PROXIMITY', angleDelta: 0, suppressAlert: false };
|
||||
}
|
||||
const camBearing = getBearing(userLat, userLng, camLat, camLng);
|
||||
const angleDelta = Math.abs((userHeading - camBearing + 180) % 360 - 180);
|
||||
|
||||
if (angleDelta < 50) {
|
||||
return { type: 'direct', label: 'DIRECT APPROACH', angleDelta, suppressAlert: false };
|
||||
} else if (angleDelta <= 130) {
|
||||
return { type: 'cross', label: 'CROSS-TRAFFIC', angleDelta, suppressAlert: false };
|
||||
} else {
|
||||
return { type: 'away', label: 'RECEDING / BEHIND', angleDelta, suppressAlert: true };
|
||||
}
|
||||
}
|
||||
|
||||
window.getBearing = getBearing;
|
||||
window.classifyApproach = classifyApproach;
|
||||
|
||||
// --- PROXIMITY CALCULATION & ALERT ENGINE ---
|
||||
function checkProximityToCameras() {
|
||||
if (!allCameras.length) return;
|
||||
@@ -809,7 +881,7 @@
|
||||
// Check if inside zone radius
|
||||
const insideZone = closestDistMeters <= (closestCam.radius || 300);
|
||||
|
||||
// Update HUD
|
||||
// Update HUD distance display
|
||||
if (distFt < 5280) {
|
||||
el.hudDistVal.textContent = distFt > 1000 ? (distFt / 1000).toFixed(1) + 'k' : distFt;
|
||||
el.hudDistSub.textContent = 'FT';
|
||||
@@ -822,18 +894,55 @@
|
||||
el.hudCamAgency.textContent = closestCam.name;
|
||||
}
|
||||
|
||||
// Update Graduated Radar Signal Strength Meter (Valentine One Style)
|
||||
updateRadarGauge(distFt, insideZone, closestCam);
|
||||
const approach = closestCam
|
||||
? classifyApproach(currentPosition.lat, currentPosition.lng, lastGpsHeading, closestCam.lat, closestCam.lng, currentSpeed)
|
||||
: { type: 'proximity', label: 'ALL CLEAR', angleDelta: 0, suppressAlert: false };
|
||||
|
||||
const cautionThreshold = (radarRangeMode === 'highway') ? 2500 : 1200;
|
||||
|
||||
// Update Directional Approach Angle Badge
|
||||
if (el.hudApproachBadge) {
|
||||
if (distFt > cautionThreshold || !closestCam) {
|
||||
el.hudApproachBadge.className = 'hud-approach-badge approach-none';
|
||||
el.hudApproachBadge.textContent = 'ALL CLEAR';
|
||||
} else {
|
||||
el.hudApproachBadge.className = `hud-approach-badge approach-${approach.type}`;
|
||||
el.hudApproachBadge.textContent = approach.label;
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic Time-to-Zone Intercept Countdown
|
||||
if (el.hudEtaVal && el.hudEtaSub) {
|
||||
if (currentSpeed >= 4 && !approach.suppressAlert && distFt <= (radarRangeMode === 'highway' ? 3500 : 2000)) {
|
||||
const speedMps = currentSpeed * 0.44704;
|
||||
const etaSec = Math.round(closestDistMeters / speedMps);
|
||||
if (etaSec <= 60) {
|
||||
el.hudEtaVal.textContent = `~${etaSec}`;
|
||||
el.hudEtaSub.textContent = 'SEC';
|
||||
} else {
|
||||
el.hudEtaVal.textContent = `~${(etaSec / 60).toFixed(1)}`;
|
||||
el.hudEtaSub.textContent = 'MIN';
|
||||
}
|
||||
} else if (approach.suppressAlert && distFt <= cautionThreshold) {
|
||||
el.hudEtaVal.textContent = '—';
|
||||
el.hudEtaSub.textContent = 'AWAY';
|
||||
} else {
|
||||
el.hudEtaVal.textContent = '—';
|
||||
el.hudEtaSub.textContent = 'IDLE';
|
||||
}
|
||||
}
|
||||
|
||||
// Update Graduated Radar Signal Strength Meter
|
||||
updateRadarGauge(distFt, insideZone, closestCam, approach);
|
||||
|
||||
// Audio Proximity Geiger-Counter Chirps
|
||||
handleProximityChirps(distFt);
|
||||
handleProximityChirps(distFt, approach);
|
||||
|
||||
// State Transitions
|
||||
const cautionThreshold = (radarRangeMode === 'highway') ? 2500 : 1200;
|
||||
if (insideZone) {
|
||||
if (!isInZone) {
|
||||
// Just entered zone! Trigger Alert!
|
||||
triggerZoneAlert(closestCam, distFt);
|
||||
triggerZoneAlert(closestCam, distFt, approach);
|
||||
}
|
||||
isInZone = true;
|
||||
el.hudZoneBadge.className = 'hud-badge badge-danger';
|
||||
@@ -843,14 +952,22 @@
|
||||
<span class="banner-pulse-icon">${window.FlockIcons ? FlockIcons.alertTriangle('flock-svg-lg') : ''}</span>
|
||||
<div>
|
||||
<strong>FLOCK CAMERA ACTIVE ZONE</strong>
|
||||
<p>${escapeHtml(closestCam.agency)} • Vehicle Profile Logging</p>
|
||||
<p>${escapeHtml(closestCam.agency)} • ${approach.label}</p>
|
||||
</div>
|
||||
`;
|
||||
} else if (distFt <= cautionThreshold) {
|
||||
// Approaching camera
|
||||
isInZone = false;
|
||||
if (approach.suppressAlert) {
|
||||
el.hudZoneBadge.className = 'hud-badge badge-clear';
|
||||
el.hudZoneBadge.innerHTML = `<span class="badge-dot dot-clear"></span> RECEDING CAMERA`;
|
||||
} else if (approach.type === 'cross') {
|
||||
el.hudZoneBadge.className = 'hud-badge badge-warning';
|
||||
el.hudZoneBadge.innerHTML = `<span class="badge-dot dot-warning"></span> CROSS-TRAFFIC CAMERA`;
|
||||
} else {
|
||||
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
|
||||
@@ -862,7 +979,7 @@
|
||||
}
|
||||
|
||||
// --- 3-STAGE GRADUATED RADAR DETECTOR SIGNAL GAUGE ---
|
||||
function updateRadarGauge(distFt, insideZone, closestCam) {
|
||||
function updateRadarGauge(distFt, insideZone, closestCam, approach) {
|
||||
if (!el.hudGaugeState || !el.hudSignalMeter) return;
|
||||
|
||||
const isHighway = (radarRangeMode === 'highway');
|
||||
@@ -874,6 +991,14 @@
|
||||
b.className = 'signal-bar';
|
||||
});
|
||||
|
||||
// If moving away from camera past immediate zone, show subdued reassurance
|
||||
if (approach && approach.suppressAlert && distFt > 350) {
|
||||
el.hudGaugeState.className = 'hud-gauge-status status-green';
|
||||
el.hudGaugeState.textContent = `RECEDING (${distFt} FT)`;
|
||||
bars[0].classList.add('active-green');
|
||||
return;
|
||||
}
|
||||
|
||||
if (insideZone || distFt <= 450) {
|
||||
// Level 5: Red Alert In Zone
|
||||
el.hudGaugeState.className = 'hud-gauge-status status-red';
|
||||
@@ -882,7 +1007,9 @@
|
||||
} else if (distFt <= warnDist) {
|
||||
// Level 4: Immediate Proximity
|
||||
el.hudGaugeState.className = 'hud-gauge-status status-red';
|
||||
el.hudGaugeState.textContent = `ALERT: ${distFt} FT`;
|
||||
el.hudGaugeState.textContent = (approach && approach.type === 'cross')
|
||||
? `CROSS-TRAFFIC: ${distFt} FT`
|
||||
: `ALERT: ${distFt} FT`;
|
||||
bars[0].classList.add('active-red');
|
||||
bars[1].classList.add('active-red');
|
||||
bars[2].classList.add('active-red');
|
||||
@@ -890,16 +1017,21 @@
|
||||
} 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)`;
|
||||
el.hudGaugeState.textContent = (approach && approach.type === 'cross')
|
||||
? `CROSS-STREET (${Math.round(distFt / 100) * 100} FT)`
|
||||
: `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
|
||||
// Pre-alert announcement once per camera approach (only if not moving away)
|
||||
if (closestCam && lastCautionSpokenCamId !== closestCam.id) {
|
||||
if (!approach || !approach.suppressAlert) {
|
||||
lastCautionSpokenCamId = closestCam.id;
|
||||
playCautionBeep();
|
||||
speakVoiceAlert(`Caution. Approaching surveillance camera in ${Math.round(distFt / 100) * 100} feet.`);
|
||||
const approachDesc = (approach && approach.type === 'cross') ? 'cross-traffic ' : '';
|
||||
speakVoiceAlert(`Caution. Approaching ${approachDesc}surveillance camera in ${Math.round(distFt / 100) * 100} feet.`);
|
||||
}
|
||||
}
|
||||
} else if (distFt <= (cautionDist * 1.6)) {
|
||||
// Level 2: Grid Range
|
||||
@@ -919,27 +1051,53 @@
|
||||
}
|
||||
}
|
||||
|
||||
// --- AUDIO SYNTHESIZERS: CHIRPS & CAUTION BEEPS ---
|
||||
function playLogConfirmationChirp() {
|
||||
try {
|
||||
// --- AUDIO SYNTHESIZERS & MASTER GAIN BOOSTER (v27) ---
|
||||
function getAudioContext() {
|
||||
if (!audioContext) {
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
}
|
||||
if (audioContext.state === 'suspended') {
|
||||
audioContext.resume();
|
||||
}
|
||||
const osc = audioContext.createOscillator();
|
||||
const gain = audioContext.createGain();
|
||||
if (!masterGainNode) {
|
||||
masterGainNode = audioContext.createGain();
|
||||
masterGainNode.gain.setValueAtTime(isAudioBoostEnabled ? 1.6 : 1.0, audioContext.currentTime);
|
||||
masterGainNode.connect(audioContext.destination);
|
||||
}
|
||||
return audioContext;
|
||||
}
|
||||
|
||||
function getAudioOutput() {
|
||||
const ctx = getAudioContext();
|
||||
return masterGainNode || ctx.destination;
|
||||
}
|
||||
|
||||
function triggerHapticFeedback(pattern = [180, 70, 180, 70, 350]) {
|
||||
if (!isHapticBoostEnabled) return;
|
||||
if ('vibrate' in navigator) {
|
||||
try {
|
||||
navigator.vibrate(pattern);
|
||||
} catch (e) {
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function playLogConfirmationChirp() {
|
||||
try {
|
||||
const ctx = getAudioContext();
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.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.frequency.setValueAtTime(587.33, ctx.currentTime); // D5
|
||||
osc.frequency.setValueAtTime(880, ctx.currentTime + 0.1); // A5
|
||||
osc.frequency.setValueAtTime(1174.66, ctx.currentTime + 0.2); // D6
|
||||
gain.gain.setValueAtTime(0.35, ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.4);
|
||||
osc.connect(gain);
|
||||
gain.connect(audioContext.destination);
|
||||
gain.connect(getAudioOutput());
|
||||
osc.start();
|
||||
osc.stop(audioContext.currentTime + 0.4);
|
||||
osc.stop(ctx.currentTime + 0.4);
|
||||
} catch (e) {
|
||||
console.warn('Log chirp error:', e);
|
||||
}
|
||||
@@ -949,22 +1107,17 @@
|
||||
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();
|
||||
const ctx = getAudioContext();
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.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.frequency.setValueAtTime(520, ctx.currentTime);
|
||||
gain.gain.setValueAtTime(0.25, ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.22);
|
||||
osc.connect(gain);
|
||||
gain.connect(audioContext.destination);
|
||||
gain.connect(getAudioOutput());
|
||||
osc.start();
|
||||
osc.stop(audioContext.currentTime + 0.22);
|
||||
osc.stop(ctx.currentTime + 0.22);
|
||||
} catch (e) {
|
||||
console.warn('Caution beep error:', e);
|
||||
}
|
||||
@@ -1047,37 +1200,37 @@
|
||||
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();
|
||||
const ctx = getAudioContext();
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(freq, audioContext.currentTime);
|
||||
osc.frequency.exponentialRampToValueAtTime(freq * 1.15, audioContext.currentTime + 0.045);
|
||||
osc.frequency.setValueAtTime(freq, ctx.currentTime);
|
||||
osc.frequency.exponentialRampToValueAtTime(freq * 1.15, ctx.currentTime + 0.045);
|
||||
|
||||
gain.gain.setValueAtTime(volume, audioContext.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.05);
|
||||
gain.gain.setValueAtTime(volume, ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.05);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(audioContext.destination);
|
||||
gain.connect(getAudioOutput());
|
||||
|
||||
osc.start();
|
||||
osc.stop(audioContext.currentTime + 0.05);
|
||||
osc.stop(ctx.currentTime + 0.05);
|
||||
} catch (e) {
|
||||
// AudioContext policy
|
||||
}
|
||||
}
|
||||
|
||||
function handleProximityChirps(distFt) {
|
||||
function handleProximityChirps(distFt, approach) {
|
||||
if (radarAudioMode === 'silent') return;
|
||||
const user = window.FlockAuth ? window.FlockAuth.getUser() : null;
|
||||
if (user && !user.soundEnabled) return;
|
||||
|
||||
// Suppress repeated geiger chirps when driving away from camera past 350ft
|
||||
if (approach && approach.suppressAlert && distFt > 350) {
|
||||
return;
|
||||
}
|
||||
|
||||
const maxChirpDist = (radarRangeMode === 'highway') ? 2200 : 1200;
|
||||
if (distFt > maxChirpDist || distFt <= 0) {
|
||||
stationaryChirpCount = 0;
|
||||
@@ -1202,30 +1355,25 @@
|
||||
lastChimeTime = now;
|
||||
|
||||
try {
|
||||
if (!audioContext) {
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
}
|
||||
if (audioContext.state === 'suspended') {
|
||||
audioContext.resume();
|
||||
}
|
||||
const ctx = getAudioContext();
|
||||
|
||||
// Synthesize clean two-tone radar alert
|
||||
const osc = audioContext.createOscillator();
|
||||
const gain = audioContext.createGain();
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(880, audioContext.currentTime);
|
||||
osc.frequency.setValueAtTime(659.25, audioContext.currentTime + 0.12);
|
||||
osc.frequency.setValueAtTime(880, audioContext.currentTime + 0.24);
|
||||
osc.frequency.setValueAtTime(880, ctx.currentTime);
|
||||
osc.frequency.setValueAtTime(659.25, ctx.currentTime + 0.12);
|
||||
osc.frequency.setValueAtTime(880, ctx.currentTime + 0.24);
|
||||
|
||||
gain.gain.setValueAtTime(0.35, audioContext.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.45);
|
||||
gain.gain.setValueAtTime(0.35, ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.45);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(audioContext.destination);
|
||||
gain.connect(getAudioOutput());
|
||||
|
||||
osc.start();
|
||||
osc.stop(audioContext.currentTime + 0.45);
|
||||
osc.stop(ctx.currentTime + 0.45);
|
||||
} catch (e) {
|
||||
console.warn('Audio chime error:', e);
|
||||
}
|
||||
@@ -1242,17 +1390,23 @@
|
||||
const utter = new SpeechSynthesisUtterance(text);
|
||||
utter.rate = 1.05;
|
||||
utter.pitch = 1.0;
|
||||
utter.volume = 1.0;
|
||||
utter.volume = isAudioBoostEnabled ? 1.0 : 0.9;
|
||||
window.speechSynthesis.speak(utter);
|
||||
} catch (e) {
|
||||
console.warn('Speech error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function triggerZoneAlert(cam, distFt) {
|
||||
function triggerZoneAlert(cam, distFt, approach) {
|
||||
if (approach && approach.suppressAlert) return;
|
||||
playRadarChime();
|
||||
triggerHapticFeedback([180, 70, 180, 70, 350]);
|
||||
if (approach && approach.type === 'cross') {
|
||||
speakVoiceAlert("Caution. Cross-traffic Flock camera active on perpendicular avenue.");
|
||||
} else {
|
||||
speakVoiceAlert("Warning. Flock camera detected ahead. You are entering a recorded surveillance zone.");
|
||||
}
|
||||
}
|
||||
|
||||
// --- VIRAL SHARING HELPER ---
|
||||
async function shareApp(customTitle, customText, customUrl) {
|
||||
@@ -2413,6 +2567,267 @@ Generated via FlockRadar Civic Transparency Network (flockradar.org)`;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// v27 DRIVER-CENTRIC POLISH REFINEMENTS
|
||||
// 1. Instant 1-Tap Driving Sighting Stamp
|
||||
// 2. Windshield Reflection Mirror Mode
|
||||
// 3. Stealth AMOLED True Black Mode
|
||||
// 4. Cabin Audio Gain Booster (+6dB)
|
||||
// 5. Steering Haptic Vibration Feedback
|
||||
// ==========================================================================
|
||||
|
||||
// --- 1-TAP "MARK CAMERA WHILE DRIVING" INSTANT SIGHTING STAMP ---
|
||||
function loadQuickStamps() {
|
||||
try {
|
||||
const stored = localStorage.getItem('flock_quick_stamps');
|
||||
savedQuickStamps = stored ? JSON.parse(stored) : [];
|
||||
} catch (e) {
|
||||
savedQuickStamps = [];
|
||||
}
|
||||
renderStampedSightings();
|
||||
renderExistingStampMarkers();
|
||||
}
|
||||
|
||||
function saveQuickStamps() {
|
||||
try {
|
||||
localStorage.setItem('flock_quick_stamps', JSON.stringify(savedQuickStamps));
|
||||
} catch (e) {
|
||||
console.warn('Failed to save quick stamps', e);
|
||||
}
|
||||
renderStampedSightings();
|
||||
}
|
||||
|
||||
function renderExistingStampMarkers() {
|
||||
if (!map) return;
|
||||
stampMarkers.forEach(m => map.removeLayer(m));
|
||||
stampMarkers = [];
|
||||
|
||||
savedQuickStamps.forEach(s => {
|
||||
const stampIcon = L.divIcon({
|
||||
className: 'stamp-marker-icon',
|
||||
html: `<div class="stamp-pulse-pin" style="width:24px; height:24px; font-size:12px;">⚡</div>`,
|
||||
iconSize: [24, 24],
|
||||
iconAnchor: [12, 12]
|
||||
});
|
||||
const marker = L.marker([s.lat, s.lng], { icon: stampIcon }).addTo(map);
|
||||
marker.bindPopup(`
|
||||
<div style="font-family:inherit; min-width:180px; font-size:0.8rem; color:#0f172a; padding:4px;">
|
||||
<strong style="color:#d97706; display:flex; align-items:center; gap:4px;">
|
||||
⚡ Stamped Sighting (Draft)
|
||||
</strong>
|
||||
<p style="margin:4px 0 6px 0; font-size:0.75rem; color:#475569;">
|
||||
Logged at: <strong>${s.timeStr}</strong><br>
|
||||
Speed: <strong>${s.speed} MPH</strong> • Heading: <strong>${s.heading}°</strong><br>
|
||||
Coords: <code>${s.lat}, ${s.lng}</code>
|
||||
</p>
|
||||
<span style="font-size:0.72rem; color:#0284c7; font-weight:600;">Saved in Local Drafts</span>
|
||||
</div>
|
||||
`);
|
||||
stampMarkers.push(marker);
|
||||
});
|
||||
}
|
||||
|
||||
function quickMarkAtGps() {
|
||||
const lat = currentPosition.lat;
|
||||
const lng = currentPosition.lng;
|
||||
const speed = currentSpeed || 0;
|
||||
const heading = lastGpsHeading || 0;
|
||||
const now = new Date();
|
||||
const timeStr = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
|
||||
const stamp = {
|
||||
id: 'stamp_' + Date.now(),
|
||||
lat: Number(lat.toFixed(6)),
|
||||
lng: Number(lng.toFixed(6)),
|
||||
speed: Math.round(speed),
|
||||
heading: Math.round(heading),
|
||||
timestamp: now.toISOString(),
|
||||
timeStr: timeStr
|
||||
};
|
||||
|
||||
savedQuickStamps.unshift(stamp);
|
||||
saveQuickStamps();
|
||||
|
||||
// Instant Audio Affirmation (Dual high-pitch chirp)
|
||||
playLogConfirmationChirp();
|
||||
|
||||
// Haptic steering vibration pulse
|
||||
triggerHapticFeedback([100, 50, 120]);
|
||||
|
||||
// Drop marker on Leaflet map
|
||||
if (map) {
|
||||
const stampIcon = L.divIcon({
|
||||
className: 'stamp-marker-icon',
|
||||
html: `<div class="stamp-pulse-pin" style="width:26px; height:26px;">⚡</div>`,
|
||||
iconSize: [26, 26],
|
||||
iconAnchor: [13, 13]
|
||||
});
|
||||
const marker = L.marker([stamp.lat, stamp.lng], { icon: stampIcon }).addTo(map);
|
||||
marker.bindPopup(`
|
||||
<div style="font-family:inherit; min-width:180px; font-size:0.8rem; color:#0f172a; padding:4px;">
|
||||
<strong style="color:#d97706; display:flex; align-items:center; gap:4px;">
|
||||
⚡ Stamped Sighting (Draft)
|
||||
</strong>
|
||||
<p style="margin:4px 0 6px 0; font-size:0.75rem; color:#475569;">
|
||||
Logged at: <strong>${timeStr}</strong><br>
|
||||
Speed: <strong>${speed} MPH</strong> • Heading: <strong>${heading}°</strong><br>
|
||||
Coords: <code>${stamp.lat}, ${stamp.lng}</code>
|
||||
</p>
|
||||
<span style="font-size:0.72rem; color:#0284c7; font-weight:600;">Saved in Local Drafts</span>
|
||||
</div>
|
||||
`).openPopup();
|
||||
stampMarkers.push(marker);
|
||||
}
|
||||
|
||||
showToast(`⚡ Camera Sighting Stamped at ${stamp.lat}, ${stamp.lng}! Saved to drafts.`);
|
||||
}
|
||||
|
||||
function clearAllStamps() {
|
||||
if (!savedQuickStamps.length) return;
|
||||
if (confirm('Clear all saved camera sighting stamps?')) {
|
||||
savedQuickStamps = [];
|
||||
saveQuickStamps();
|
||||
stampMarkers.forEach(m => {
|
||||
if (map && m) map.removeLayer(m);
|
||||
});
|
||||
stampMarkers = [];
|
||||
showToast('Cleared all local camera stamps.');
|
||||
}
|
||||
}
|
||||
|
||||
function renderStampedSightings() {
|
||||
if (el.stampsCount) {
|
||||
el.stampsCount.textContent = savedQuickStamps.length;
|
||||
}
|
||||
if (!el.stampsListContainer) return;
|
||||
|
||||
if (!savedQuickStamps.length) {
|
||||
el.stampsListContainer.innerHTML = `<p class="stamps-empty-hint">No quick stamps yet. Tap ⚡ Stamp while driving to record GPS instantly.</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
el.stampsListContainer.innerHTML = savedQuickStamps.map(s => `
|
||||
<div class="stamp-item-card" data-id="${s.id}">
|
||||
<div class="stamp-item-left">
|
||||
<span class="stamp-badge-zap">⚡</span>
|
||||
<div>
|
||||
<strong>${s.timeStr}</strong> (${s.speed} MPH)
|
||||
<div style="font-size:0.72rem; color:#94a3b8;">${s.lat.toFixed(4)}, ${s.lng.toFixed(4)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn-mini-link" style="color:#38bdf8; background:none; border:none; cursor:pointer; font-size:0.72rem; font-weight:600;" onclick="window.zoomToStamp(${s.lat}, ${s.lng})">Zoom</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
window.zoomToStamp = function(lat, lng) {
|
||||
if (map) {
|
||||
map.setView([lat, lng], 17, { animate: true });
|
||||
showToast(`Zoomed to stamped camera at ${lat}, ${lng}`);
|
||||
}
|
||||
};
|
||||
|
||||
// --- WINDSHIELD REFLECTION MIRROR MODE ---
|
||||
function applyHudMirror(enabled, showFeedback = true) {
|
||||
isHudMirrored = enabled;
|
||||
localStorage.setItem('flock_hud_mirror', enabled ? 'true' : 'false');
|
||||
if (el.driverHud) {
|
||||
el.driverHud.classList.toggle('mirror-hud', enabled);
|
||||
}
|
||||
if (el.btnMirrorHud) {
|
||||
el.btnMirrorHud.classList.toggle('active', enabled);
|
||||
}
|
||||
if (el.mirrorHudLabel) {
|
||||
el.mirrorHudLabel.textContent = enabled ? 'Windshield Mirror: ON' : 'Windshield Mirror: OFF';
|
||||
}
|
||||
if (showFeedback) {
|
||||
if (enabled) {
|
||||
showToast('🪞 Windshield HUD Mirror ON: Lay phone flat on dashboard under glass!');
|
||||
} else {
|
||||
showToast('🪞 Windshield HUD Mirror OFF');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMirrorHud() {
|
||||
applyHudMirror(!isHudMirrored, true);
|
||||
}
|
||||
|
||||
// --- STEALTH AMOLED NIGHT MODE (TRUE #000000) ---
|
||||
function applyStealthAmoled(enabled, showFeedback = true) {
|
||||
isStealthAmoled = enabled;
|
||||
localStorage.setItem('flock_stealth_amoled', enabled ? 'true' : 'false');
|
||||
document.body.classList.toggle('stealth-amoled', enabled);
|
||||
if (el.btnStealthAmoled) {
|
||||
el.btnStealthAmoled.classList.toggle('active', enabled);
|
||||
}
|
||||
if (el.stealthAmoledLabel) {
|
||||
el.stealthAmoledLabel.textContent = enabled ? 'AMOLED Pitch Dark: ON' : 'AMOLED Pitch Dark: OFF';
|
||||
}
|
||||
if (showFeedback) {
|
||||
if (enabled) {
|
||||
showToast('🌙 AMOLED Stealth Mode Activated: Zero Cockpit Glare (True #000000)');
|
||||
} else {
|
||||
showToast('☀️ AMOLED Stealth Mode Deactivated');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleStealthAmoled() {
|
||||
applyStealthAmoled(!isStealthAmoled, true);
|
||||
}
|
||||
|
||||
// --- CABIN AUDIO GAIN BOOSTER (+6dB) ---
|
||||
function applyAudioBoost(enabled, showFeedback = true) {
|
||||
isAudioBoostEnabled = enabled;
|
||||
localStorage.setItem('flock_radar_audio_boost', enabled ? 'true' : 'false');
|
||||
if (audioContext && masterGainNode) {
|
||||
masterGainNode.gain.setValueAtTime(enabled ? 1.6 : 1.0, audioContext.currentTime);
|
||||
}
|
||||
if (el.btnToggleAudioBoost) {
|
||||
el.btnToggleAudioBoost.classList.toggle('active', enabled);
|
||||
}
|
||||
if (el.audioBoostLabel) {
|
||||
el.audioBoostLabel.textContent = enabled ? 'Audio Boost (+6dB): ON' : 'Audio Boost (+6dB): OFF';
|
||||
}
|
||||
if (showFeedback) {
|
||||
playRadarTick(enabled ? 1100 : 700, 0.3);
|
||||
if (enabled) {
|
||||
showToast('🔊 Cabin Audio Boost (+6dB / 160%) ENABLED');
|
||||
} else {
|
||||
showToast('🔈 Audio Volume Set to Standard Level');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAudioBoost() {
|
||||
applyAudioBoost(!isAudioBoostEnabled, true);
|
||||
}
|
||||
|
||||
// --- HAPTIC STEERING VIBRATION FEEDBACK ---
|
||||
function applyHapticBoost(enabled, showFeedback = true) {
|
||||
isHapticBoostEnabled = enabled;
|
||||
localStorage.setItem('flock_radar_haptic_boost', enabled ? 'true' : 'false');
|
||||
if (el.btnToggleHapticBoost) {
|
||||
el.btnToggleHapticBoost.classList.toggle('active', enabled);
|
||||
}
|
||||
if (el.hapticBoostLabel) {
|
||||
el.hapticBoostLabel.textContent = enabled ? 'Steering Haptics: ON' : 'Steering Haptics: OFF';
|
||||
}
|
||||
if (showFeedback) {
|
||||
if (enabled) {
|
||||
triggerHapticFeedback([100, 50, 150]);
|
||||
showToast('📳 Haptic Steering Vibration Alerts ENABLED');
|
||||
} else {
|
||||
showToast('📴 Steering Haptic Alerts Disabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleHapticBoost() {
|
||||
applyHapticBoost(!isHapticBoostEnabled, true);
|
||||
}
|
||||
|
||||
// --- SURVEILLANCE ZONE HARDWARE PROOF MODAL ---
|
||||
function openCameraDetailModal(cam) {
|
||||
if (!cam) return;
|
||||
@@ -2944,6 +3359,14 @@ Generated via FlockRadar Civic Transparency Network (flockradar.org)`;
|
||||
if (el.btnSensCity) el.btnSensCity.addEventListener('click', () => setRadarSensitivity('city'));
|
||||
if (el.btnSensHwy) el.btnSensHwy.addEventListener('click', () => setRadarSensitivity('highway'));
|
||||
|
||||
// v27 Driving Polish & Cockpit Listeners
|
||||
if (el.btnQuickMarkDash) el.btnQuickMarkDash.addEventListener('click', quickMarkAtGps);
|
||||
if (el.btnClearStamps) el.btnClearStamps.addEventListener('click', clearAllStamps);
|
||||
if (el.btnMirrorHud) el.btnMirrorHud.addEventListener('click', toggleMirrorHud);
|
||||
if (el.btnStealthAmoled) el.btnStealthAmoled.addEventListener('click', toggleStealthAmoled);
|
||||
if (el.btnToggleAudioBoost) el.btnToggleAudioBoost.addEventListener('click', toggleAudioBoost);
|
||||
if (el.btnToggleHapticBoost) el.btnToggleHapticBoost.addEventListener('click', toggleHapticBoost);
|
||||
|
||||
// Camera Hardware Proof & Voting Events
|
||||
if (el.camDetailCloseBtn) el.camDetailCloseBtn.addEventListener('click', closeCameraDetailModal);
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
// Service Worker for FlockRadar Offline Driving & Live Updates
|
||||
const CACHE_NAME = 'flockradar-v26';
|
||||
const CACHE_NAME = 'flockradar-v27';
|
||||
const ASSETS_TO_CACHE = [
|
||||
'./',
|
||||
'./index.html',
|
||||
'./css/styles.css?v=26',
|
||||
'./js/icons.js?v=26',
|
||||
'./js/app.js?v=26',
|
||||
'./js/auth.js?v=26',
|
||||
'./js/radar.js?v=26',
|
||||
'./data/portals.js?v=26',
|
||||
'./favicon.svg?v=26',
|
||||
'./favicon.ico?v=26',
|
||||
'./apple-touch-icon.png?v=26',
|
||||
'./icon-192.png?v=26',
|
||||
'./icon-512.png?v=26',
|
||||
'./og-image.png?v=26',
|
||||
'./css/styles.css?v=27',
|
||||
'./js/icons.js?v=27',
|
||||
'./js/app.js?v=27',
|
||||
'./js/auth.js?v=27',
|
||||
'./js/radar.js?v=27',
|
||||
'./data/portals.js?v=27',
|
||||
'./favicon.svg?v=27',
|
||||
'./favicon.ico?v=27',
|
||||
'./apple-touch-icon.png?v=27',
|
||||
'./icon-192.png?v=27',
|
||||
'./icon-512.png?v=27',
|
||||
'./og-image.png?v=27',
|
||||
'./assets/img/logo.svg',
|
||||
'./assets/img/flock_pole_cam_palmdale.jpg',
|
||||
'./manifest.json'
|
||||
|
||||
Reference in New Issue
Block a user