fix(map): enable fluid mobile pinch-to-zoom and tactile zoom controls without GPS follow lock (v20)

This commit is contained in:
2026-09-12 11:35:31 -07:00
parent 5da020d563
commit b21a306790
4 changed files with 202 additions and 38 deletions
+82 -2
View File
@@ -2661,9 +2661,51 @@ body {
display: none; display: none;
} }
/* Leaflet Controls minimal */ /* Tactile Mobile Zoom Controls (+ / -) */
.leaflet-bottom.leaflet-left {
bottom: max(75px, calc(15px + env(safe-area-inset-bottom))) !important;
left: 12px !important;
z-index: 800 !important;
margin: 0 !important;
}
.leaflet-control-zoom { .leaflet-control-zoom {
display: none !important; /* Touch pinch-to-zoom is native */ display: flex !important;
flex-direction: column !important;
border: none !important;
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.65) !important;
border-radius: 12px !important;
overflow: hidden !important;
background: rgba(15, 23, 42, 0.92) !important;
backdrop-filter: blur(12px) !important;
-webkit-backdrop-filter: blur(12px) !important;
border: 1px solid rgba(56, 189, 248, 0.35) !important;
}
.leaflet-control-zoom a {
background: transparent !important;
color: #38bdf8 !important;
border: none !important;
border-bottom: 1px solid rgba(56, 189, 248, 0.15) !important;
width: 38px !important;
height: 38px !important;
line-height: 38px !important;
font-size: 20px !important;
font-weight: 700 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
text-decoration: none !important;
}
.leaflet-control-zoom a:last-child {
border-bottom: none !important;
}
.leaflet-control-zoom a:hover,
.leaflet-control-zoom a:active {
background: rgba(56, 189, 248, 0.25) !important;
color: #ffffff !important;
} }
/* Native Mobile Bottom Sheet for all Modals */ /* Native Mobile Bottom Sheet for all Modals */
@@ -2725,6 +2767,44 @@ body {
color: #38bdf8 !important; color: #38bdf8 !important;
} }
.leaflet-control-zoom {
border: none !important;
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.65) !important;
border-radius: 12px !important;
overflow: hidden !important;
background: rgba(15, 23, 42, 0.9) !important;
backdrop-filter: blur(12px) !important;
-webkit-backdrop-filter: blur(12px) !important;
border: 1px solid rgba(56, 189, 248, 0.35) !important;
}
.leaflet-control-zoom a {
background: transparent !important;
color: #38bdf8 !important;
border: none !important;
border-bottom: 1px solid rgba(56, 189, 248, 0.15) !important;
width: 36px !important;
height: 36px !important;
line-height: 36px !important;
font-size: 18px !important;
font-weight: 700 !important;
transition: all 0.15s ease !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
text-decoration: none !important;
}
.leaflet-control-zoom a:last-child {
border-bottom: none !important;
}
.leaflet-control-zoom a:hover,
.leaflet-control-zoom a:active {
background: rgba(56, 189, 248, 0.25) !important;
color: #ffffff !important;
}
/* ========================================================================== /* ==========================================================================
5% DRIVING APP UPGRADES: RADAR GAUGE, 1-TAP FAB, RECENTER, SENSITIVITY 5% DRIVING APP UPGRADES: RADAR GAUGE, 1-TAP FAB, RECENTER, SENSITIVITY
========================================================================== */ ========================================================================== */
+10 -6
View File
@@ -54,7 +54,7 @@
<meta name="apple-mobile-web-app-title" content="FlockRadar"> <meta name="apple-mobile-web-app-title" content="FlockRadar">
<!-- Main Stylesheet --> <!-- Main Stylesheet -->
<link rel="stylesheet" href="css/styles.css?v=19"> <link rel="stylesheet" href="css/styles.css?v=20">
</head> </head>
<body> <body>
@@ -283,6 +283,10 @@
<svg class="flock-svg flock-svg-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg> <svg class="flock-svg flock-svg-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
<span>West AV</span> <span>West AV</span>
</button> </button>
<button id="btn-loc-overview" class="loc-chip" title="Zoom out to see all Antelope Valley cameras">
<svg class="flock-svg flock-svg-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
<span>Overview (All AV)</span>
</button>
</div> </div>
<form id="map-search-form" class="map-search-box"> <form id="map-search-form" class="map-search-box">
<input type="text" id="map-search-input" placeholder="Search Antelope Valley street or address..." autocomplete="off"> <input type="text" id="map-search-input" placeholder="Search Antelope Valley street or address..." autocomplete="off">
@@ -1299,11 +1303,11 @@
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script> <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 --> <!-- Data & Application Logic with Cache Busting -->
<script src="data/portals.js?v=19"></script> <script src="data/portals.js?v=20"></script>
<script src="js/icons.js?v=19"></script> <script src="js/icons.js?v=20"></script>
<script src="js/auth.js?v=19"></script> <script src="js/auth.js?v=20"></script>
<script src="js/radar.js?v=19"></script> <script src="js/radar.js?v=20"></script>
<script src="js/app.js?v=19"></script> <script src="js/app.js?v=20"></script>
<!-- Service Worker Instant Cache Invalidation --> <!-- Service Worker Instant Cache Invalidation -->
<script> <script>
+98 -18
View File
@@ -92,6 +92,8 @@
// New 5% Driving Mode Enhancements // New 5% Driving Mode Enhancements
let isFollowCarLocked = true; let isFollowCarLocked = true;
let isUserInteractingWithMap = false;
let touchInteractionTimer = null;
let radarRangeMode = 'city'; // 'city' (1,000 ft) or 'highway' (2,500 ft) let radarRangeMode = 'city'; // 'city' (1,000 ft) or 'highway' (2,500 ft)
let lastCautionSpokenCamId = null; let lastCautionSpokenCamId = null;
@@ -129,6 +131,7 @@
btnLocPalmdaleEast: document.getElementById('btn-loc-palmdale-east'), btnLocPalmdaleEast: document.getElementById('btn-loc-palmdale-east'),
btnLocPalmdaleSouth: document.getElementById('btn-loc-palmdale-south'), btnLocPalmdaleSouth: document.getElementById('btn-loc-palmdale-south'),
btnLocRancho: document.getElementById('btn-loc-rancho'), btnLocRancho: document.getElementById('btn-loc-rancho'),
btnLocOverview: document.getElementById('btn-loc-overview'),
mapSearchForm: document.getElementById('map-search-form'), mapSearchForm: document.getElementById('map-search-form'),
mapSearchInput: document.getElementById('map-search-input'), mapSearchInput: document.getElementById('map-search-input'),
@@ -268,18 +271,26 @@
// --- LEAFLET MAP SETUP (HIGH-RESOLUTION ZERO-WATERMARK RADAR DARK MODE) --- // --- LEAFLET MAP SETUP (HIGH-RESOLUTION ZERO-WATERMARK RADAR DARK MODE) ---
function initLeafletMap() { function initLeafletMap() {
// Center initially on Palmdale, California // Center initially on Palmdale, California with full responsive touch zoom
map = L.map('radar-map', { map = L.map('radar-map', {
center: [34.5950, -118.1350], center: [34.5950, -118.1350],
zoom: 14, zoom: 14,
minZoom: 3,
maxZoom: 19,
zoomControl: false, zoomControl: false,
attributionControl: false attributionControl: false,
touchZoom: true,
bounceAtZoomLimits: false,
doubleClickZoom: true,
scrollWheelZoom: true,
dragging: true
}); });
window.flockMap = map; window.flockMap = map;
// High-Resolution Radar Dark Tiles (Zero API Key, Zero Watermarks, Sub-Meter Street Clarity up to Zoom 19) // 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', { darkTileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
subdomains: 'abc', subdomains: 'abc',
minZoom: 3,
maxZoom: 19, maxZoom: 19,
className: 'dark-radar-tile', className: 'dark-radar-tile',
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>' attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
@@ -288,6 +299,7 @@
// Prepare optional Satellite Layer (Zero API Key, Zero Watermarks) // 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}', { satelliteLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
maxNativeZoom: 18, maxNativeZoom: 18,
minZoom: 3,
maxZoom: 19, maxZoom: 19,
attribution: '&copy; Esri, Maxar, Earthstar Geographics' attribution: '&copy; Esri, Maxar, Earthstar Geographics'
}); });
@@ -295,18 +307,81 @@
// High-contrast street labels overlay for satellite view (Zero Watermarks) // 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}', { satelliteLabelsLayer = L.tileLayer('https://services.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}', {
maxNativeZoom: 18, maxNativeZoom: 18,
minZoom: 3,
maxZoom: 19 maxZoom: 19
}); });
// Minimal zoom controls on bottom-left // Tactile radar zoom controls (+ / -) on bottom-left
L.control.zoom({ position: 'bottomleft' }).addTo(map); L.control.zoom({ position: 'bottomleft' }).addTo(map);
// When the user drags the map, pause auto-centering and show Recenter button function setMapInteracting(active, delay = 0) {
map.on('dragstart', () => { clearTimeout(touchInteractionTimer);
if (delay > 0) {
touchInteractionTimer = setTimeout(() => {
isUserInteractingWithMap = false;
}, delay);
} else {
isUserInteractingWithMap = active;
}
}
// Direct multi-touch gesture detection on map container
const mapEl = document.getElementById('radar-map');
if (mapEl) {
mapEl.addEventListener('touchstart', (e) => {
setMapInteracting(true);
if (e.touches && e.touches.length >= 2) {
// Pinch-to-zoom gesture in progress
if (isFollowCarLocked) { if (isFollowCarLocked) {
isFollowCarLocked = false; isFollowCarLocked = false;
updateFollowCarUI(); updateFollowCarUI();
} }
}
}, { passive: true });
mapEl.addEventListener('touchmove', (e) => {
setMapInteracting(true);
if (e.touches && e.touches.length >= 2) {
if (isFollowCarLocked) {
isFollowCarLocked = false;
updateFollowCarUI();
}
}
}, { passive: true });
mapEl.addEventListener('touchend', () => {
setMapInteracting(false, 600);
}, { passive: true });
mapEl.addEventListener('touchcancel', () => {
setMapInteracting(false, 600);
}, { passive: true });
}
// Leaflet drag and zoom gesture hooks
map.on('dragstart', () => {
setMapInteracting(true);
if (isFollowCarLocked) {
isFollowCarLocked = false;
updateFollowCarUI();
}
});
map.on('dragend', () => {
setMapInteracting(false, 500);
});
map.on('zoomstart', () => {
setMapInteracting(true);
// Unlock auto-center so user can freely inspect cameras at whatever zoom level they pick
if (isFollowCarLocked) {
isFollowCarLocked = false;
updateFollowCarUI();
}
});
map.on('zoomend', () => {
setMapInteracting(false, 500);
}); });
} }
@@ -726,7 +801,7 @@
isFollowCarLocked = !isFollowCarLocked; isFollowCarLocked = !isFollowCarLocked;
updateFollowCarUI(); updateFollowCarUI();
if (isFollowCarLocked) { if (isFollowCarLocked) {
map.setView([currentPosition.lat, currentPosition.lng], Math.max(map.getZoom(), 15), { animate: true }); map.panTo([currentPosition.lat, currentPosition.lng], { animate: true, duration: 0.5 });
} }
} }
@@ -734,7 +809,7 @@
isFollowCarLocked = true; isFollowCarLocked = true;
updateFollowCarUI(); updateFollowCarUI();
if (!isContinuousTracking && navigator.geolocation) { if (!isContinuousTracking && navigator.geolocation) {
startContinuousGpsTracking({ forceRecenter: true, flyTo: true }); startContinuousGpsTracking({ forceRecenter: true, centerMap: true });
} else { } else {
map.panTo([currentPosition.lat, currentPosition.lng], { animate: true, duration: 0.5 }); map.panTo([currentPosition.lat, currentPosition.lng], { animate: true, duration: 0.5 });
} }
@@ -1063,8 +1138,8 @@
} }
if (options.flyTo) { if (options.flyTo) {
map.flyTo([curLat, curLng], 16, { duration: 1.0 }); map.flyTo([curLat, curLng], map.getZoom() || 15, { duration: 1.0 });
} else if (isFollowCarLocked) { } else if (isFollowCarLocked && !isUserInteractingWithMap) {
map.panTo([curLat, curLng], { animate: true, duration: 0.5 }); map.panTo([curLat, curLng], { animate: true, duration: 0.5 });
} }
@@ -1148,12 +1223,9 @@
userMarker.setLatLng([curLat, curLng]); userMarker.setLatLng([curLat, curLng]);
} }
// Map Following // Map Following (smooth tracking at user's chosen zoom level, paused during active pinch/drag)
if (isFollowCarLocked) { if (isFollowCarLocked && !isUserInteractingWithMap) {
map.panTo([curLat, curLng], { animate: true, duration: 0.5 }); map.panTo([curLat, curLng], { animate: true, duration: 0.5 });
if (currentSpeed > 25 && map.getZoom() < 15) {
map.setZoom(15);
}
} }
// Recalculate camera proximity immediately on every movement step // Recalculate camera proximity immediately on every movement step
@@ -1227,7 +1299,7 @@
isContinuousTracking = true; isContinuousTracking = true;
updateTrackingUI(true); updateTrackingUI(true);
if (lastGpsPosition && (options.forceRecenter || options.centerMap)) { if (lastGpsPosition && (options.forceRecenter || options.centerMap)) {
map.setView([lastGpsPosition.lat, lastGpsPosition.lng], Math.max(map.getZoom(), 15), { animate: true }); map.panTo([lastGpsPosition.lat, lastGpsPosition.lng], { animate: true, duration: 0.5 });
} }
return; return;
} }
@@ -1367,8 +1439,8 @@
carElem.style.transform = `rotate(${angle}deg)`; carElem.style.transform = `rotate(${angle}deg)`;
} }
// Smoothly pan map along if follow car is locked // Smoothly pan map along if follow car is locked and user is not gesturing
if (isFollowCarLocked) { if (isFollowCarLocked && !isUserInteractingWithMap) {
map.panTo([currentPosition.lat, currentPosition.lng], { animate: false }); map.panTo([currentPosition.lat, currentPosition.lng], { animate: false });
} }
@@ -1519,7 +1591,7 @@
if (lastGpsPosition) { if (lastGpsPosition) {
isFollowCarLocked = true; isFollowCarLocked = true;
updateFollowCarUI(); updateFollowCarUI();
map.flyTo([lastGpsPosition.lat, lastGpsPosition.lng], Math.max(map.getZoom(), 16), { duration: 1.0 }); map.flyTo([lastGpsPosition.lat, lastGpsPosition.lng], map.getZoom() || 15, { duration: 1.0 });
checkProximityToCameras(); checkProximityToCameras();
} }
@@ -2178,6 +2250,14 @@
jumpToLocation(34.63179, -118.12594, 'West AV (Rancho Vista & AV Mall)'); jumpToLocation(34.63179, -118.12594, 'West AV (Rancho Vista & AV Mall)');
}); });
} }
if (el.btnLocOverview) {
el.btnLocOverview.addEventListener('click', () => {
isFollowCarLocked = false;
updateFollowCarUI();
map.flyTo([34.6300, -118.1300], 11, { duration: 1.0 });
showToast('Zoomed out: Showing all Antelope Valley Flock cameras');
});
}
// Address & City Search // Address & City Search
if (el.mapSearchForm) { if (el.mapSearchForm) {
+13 -13
View File
@@ -1,20 +1,20 @@
// Service Worker for FlockRadar Offline Driving & Live Updates // Service Worker for FlockRadar Offline Driving & Live Updates
const CACHE_NAME = 'flockradar-v19'; const CACHE_NAME = 'flockradar-v20';
const ASSETS_TO_CACHE = [ const ASSETS_TO_CACHE = [
'./', './',
'./index.html', './index.html',
'./css/styles.css?v=19', './css/styles.css?v=20',
'./js/icons.js?v=19', './js/icons.js?v=20',
'./js/app.js?v=19', './js/app.js?v=20',
'./js/auth.js?v=19', './js/auth.js?v=20',
'./js/radar.js?v=19', './js/radar.js?v=20',
'./data/portals.js?v=19', './data/portals.js?v=20',
'./favicon.svg?v=19', './favicon.svg?v=20',
'./favicon.ico?v=19', './favicon.ico?v=20',
'./apple-touch-icon.png?v=19', './apple-touch-icon.png?v=20',
'./icon-192.png?v=19', './icon-192.png?v=20',
'./icon-512.png?v=19', './icon-512.png?v=20',
'./og-image.png?v=19', './og-image.png?v=20',
'./assets/img/logo.svg', './assets/img/logo.svg',
'./assets/img/flock_pole_cam_palmdale.jpg', './assets/img/flock_pole_cam_palmdale.jpg',
'./manifest.json' './manifest.json'