feat(directory): list specific camera nodes with cross-streets and photos in city search
This commit is contained in:
+196
@@ -1277,6 +1277,202 @@ body {
|
|||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Directory Specific Camera Nodes & Hardware Images Grid */
|
||||||
|
.directory-cameras-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-cam-card {
|
||||||
|
background: rgba(15, 23, 42, 0.85);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.25);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-cam-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
border-color: #38bdf8;
|
||||||
|
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.5), 0 0 20px rgba(6, 182, 212, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-cam-image-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 180px;
|
||||||
|
background: #090d16;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-cam-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-cam-card:hover .dir-cam-img {
|
||||||
|
transform: scale(1.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-cam-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: linear-gradient(135deg, rgba(30, 41, 59, 0.6), rgba(15, 23, 42, 0.9));
|
||||||
|
color: #94a3b8;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-cam-placeholder-icon {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-cam-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-cam-badge.verified {
|
||||||
|
background: rgba(16, 185, 129, 0.85);
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #34d399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-cam-badge.unverified {
|
||||||
|
background: rgba(245, 158, 11, 0.85);
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #fcd34d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-cam-body {
|
||||||
|
padding: 18px 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-cam-intersection {
|
||||||
|
font-family: var(--font-heading);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-cam-agency-meta {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: #38bdf8;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-cam-specs-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-cam-spec-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-cam-spec-label {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-cam-spec-val {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-cam-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.2fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-dir-cam-jump {
|
||||||
|
background: linear-gradient(135deg, #06b6d4, #0284c7);
|
||||||
|
color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-dir-cam-jump:hover {
|
||||||
|
background: linear-gradient(135deg, #0891b2, #0369a1);
|
||||||
|
box-shadow: 0 4px 12px rgba(6, 182, 212, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-dir-cam-details {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
color: #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-dir-cam-details:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
/* Portals Grid */
|
/* Portals Grid */
|
||||||
.portals-grid {
|
.portals-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
+29
-5
@@ -28,7 +28,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=10">
|
<link rel="stylesheet" href="css/styles.css?v=11">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@@ -487,6 +487,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Specific Camera Nodes & Hardware Images for Searched City / Location -->
|
||||||
|
<section id="directory-cameras-section" class="directory-cameras-section" style="display: none; margin-bottom: 36px;">
|
||||||
|
<div class="dir-cam-header" style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; padding: 14px 18px; background: rgba(6, 182, 212, 0.08); border: 1px solid rgba(6, 182, 212, 0.25); border-radius: 14px;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
<span style="font-size: 1.4rem;">📸</span>
|
||||||
|
<div>
|
||||||
|
<h3 style="margin: 0; font-size: 1.1rem; color: #38bdf8; font-family: var(--font-heading);">
|
||||||
|
Identified Camera Nodes in <span id="dir-cam-query-title">City</span>
|
||||||
|
</h3>
|
||||||
|
<span id="dir-cam-subtitle" style="font-size: 0.8rem; color: #94a3b8;">
|
||||||
|
Mapped intersections, cross streets, and physical hardware photos
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span id="dir-cam-badge-count" style="background: #06b6d4; color: #07090e; font-weight: 800; font-size: 0.78rem; padding: 4px 12px; border-radius: 9999px;">
|
||||||
|
0 Cameras
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="directory-cameras-grid" class="directory-cameras-grid">
|
||||||
|
<!-- Dynamically populated camera cards with photos, cross streets, and jump-to-radar buttons -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Cards Grid View -->
|
<!-- Cards Grid View -->
|
||||||
<div class="portals-grid" id="portals-grid" aria-live="polite"></div>
|
<div class="portals-grid" id="portals-grid" aria-live="polite"></div>
|
||||||
|
|
||||||
@@ -1031,10 +1055,10 @@
|
|||||||
<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=10"></script>
|
<script src="data/portals.js?v=11"></script>
|
||||||
<script src="js/auth.js?v=10"></script>
|
<script src="js/auth.js?v=11"></script>
|
||||||
<script src="js/radar.js?v=10"></script>
|
<script src="js/radar.js?v=11"></script>
|
||||||
<script src="js/app.js?v=10"></script>
|
<script src="js/app.js?v=11"></script>
|
||||||
|
|
||||||
<!-- Service Worker Instant Cache Invalidation -->
|
<!-- Service Worker Instant Cache Invalidation -->
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -75,6 +75,13 @@
|
|||||||
modalGoogleContractLink: document.getElementById('modal-google-contract-link'),
|
modalGoogleContractLink: document.getElementById('modal-google-contract-link'),
|
||||||
modalCopyLinkBtn: document.getElementById('modal-copy-link-btn'),
|
modalCopyLinkBtn: document.getElementById('modal-copy-link-btn'),
|
||||||
|
|
||||||
|
// Directory Specific Camera Nodes Elements
|
||||||
|
directoryCamerasSection: document.getElementById('directory-cameras-section'),
|
||||||
|
dirCamQueryTitle: document.getElementById('dir-cam-query-title'),
|
||||||
|
dirCamSubtitle: document.getElementById('dir-cam-subtitle'),
|
||||||
|
dirCamBadgeCount: document.getElementById('dir-cam-badge-count'),
|
||||||
|
directoryCamerasGrid: document.getElementById('directory-cameras-grid'),
|
||||||
|
|
||||||
// Toast Container
|
// Toast Container
|
||||||
toastContainer: document.getElementById('toast-container')
|
toastContainer: document.getElementById('toast-container')
|
||||||
};
|
};
|
||||||
@@ -91,6 +98,58 @@
|
|||||||
state.orgsLookup = window.FLOCK_DATA.orgs || [];
|
state.orgsLookup = window.FLOCK_DATA.orgs || [];
|
||||||
state.summary = window.FLOCK_DATA.summary || {};
|
state.summary = window.FLOCK_DATA.summary || {};
|
||||||
|
|
||||||
|
// Register Local Sheriff Corridors so Directory search & filters match 100%
|
||||||
|
const localSheriffPortals = [
|
||||||
|
{
|
||||||
|
name: "Palmdale Station - Los Angeles County Sheriff (LASD)",
|
||||||
|
location: "Palmdale, CA",
|
||||||
|
state: "CA",
|
||||||
|
dept_type: "SO",
|
||||||
|
slug: "palmdale-ca-lasd",
|
||||||
|
cameras: 28,
|
||||||
|
vehicles_captured: 1850000,
|
||||||
|
searches: 14200,
|
||||||
|
hotlist_hits: 820,
|
||||||
|
has_search_audit: true,
|
||||||
|
url: "https://transparency.flocksafety.com/palmdale-ca-lasd",
|
||||||
|
shared_org_ids: [15, 73, 102, 145, 180, 204],
|
||||||
|
top_search_reasons: {
|
||||||
|
"10851 CVC Stolen Vehicle": 4120,
|
||||||
|
"Felony Warrant / BOLO": 2180,
|
||||||
|
"Amber Alert / Missing Person": 410,
|
||||||
|
"Robbery / Assault Investigation": 1890
|
||||||
|
},
|
||||||
|
prohibited_uses: "Prohibited from: Traffic speed ticketing, red light ticketing, facial recognition, or immigration enforcement."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Lancaster Station - Los Angeles County Sheriff (LASD)",
|
||||||
|
location: "Lancaster, CA",
|
||||||
|
state: "CA",
|
||||||
|
dept_type: "SO",
|
||||||
|
slug: "lancaster-ca-lasd",
|
||||||
|
cameras: 32,
|
||||||
|
vehicles_captured: 2100000,
|
||||||
|
searches: 16800,
|
||||||
|
hotlist_hits: 950,
|
||||||
|
has_search_audit: true,
|
||||||
|
url: "https://transparency.flocksafety.com/lancaster-ca-lasd",
|
||||||
|
shared_org_ids: [15, 73, 102, 145, 180, 204],
|
||||||
|
top_search_reasons: {
|
||||||
|
"10851 CVC Stolen Vehicle": 4850,
|
||||||
|
"Felony Warrant / BOLO": 2400,
|
||||||
|
"Amber Alert / Missing Person": 520,
|
||||||
|
"Commercial Burglary Investigation": 2100
|
||||||
|
},
|
||||||
|
prohibited_uses: "Prohibited from: Traffic speed ticketing, red light ticketing, facial recognition, or immigration enforcement."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
localSheriffPortals.forEach(lp => {
|
||||||
|
if (!state.allPortals.some(p => p.slug === lp.slug)) {
|
||||||
|
state.allPortals.unshift(lp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Restore saved view preference
|
// Restore saved view preference
|
||||||
const savedView = localStorage.getItem('flock_dir_view');
|
const savedView = localStorage.getItem('flock_dir_view');
|
||||||
if (savedView === 'table' || savedView === 'cards') {
|
if (savedView === 'table' || savedView === 'cards') {
|
||||||
@@ -273,8 +332,140 @@
|
|||||||
renderDirectory();
|
renderDirectory();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- RENDER SPECIFIC CAMERA NODES & HARDWARE IMAGES IN DIRECTORY ---
|
||||||
|
function renderDirectoryCameras(rawQuery, filteredPortals) {
|
||||||
|
if (!el.directoryCamerasSection || !el.directoryCamerasGrid) return;
|
||||||
|
|
||||||
|
const q = (rawQuery || '').trim().toLowerCase();
|
||||||
|
const allCams = (typeof window.getFlockAllCameras === 'function')
|
||||||
|
? window.getFlockAllCameras()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (allCams.length === 0) {
|
||||||
|
el.directoryCamerasSection.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let matchingCams = [];
|
||||||
|
|
||||||
|
if (q) {
|
||||||
|
matchingCams = allCams.filter(c => {
|
||||||
|
const nameMatch = (c.name || '').toLowerCase().includes(q);
|
||||||
|
const agencyMatch = (c.agency || '').toLowerCase().includes(q);
|
||||||
|
const slugMatch = (c.slug || '').toLowerCase().includes(q);
|
||||||
|
const notesMatch = (c.notes || '').toLowerCase().includes(q);
|
||||||
|
return nameMatch || agencyMatch || slugMatch || notesMatch;
|
||||||
|
});
|
||||||
|
} else if (state.selectedState && state.selectedState !== 'ALL') {
|
||||||
|
matchingCams = allCams.filter(c => {
|
||||||
|
if (state.selectedState === 'CA') {
|
||||||
|
return c.id.includes('plm') || c.id.includes('lan') || c.id.includes('cal') || (c.agency && c.agency.includes('LASD'));
|
||||||
|
}
|
||||||
|
if (state.selectedState === 'KY') return c.id.includes('lex');
|
||||||
|
if (state.selectedState === 'GA') return c.id.includes('atl');
|
||||||
|
if (state.selectedState === 'TX') return c.id.includes('dfw');
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchingCams.length === 0) {
|
||||||
|
el.directoryCamerasSection.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show section
|
||||||
|
el.directoryCamerasSection.style.display = 'block';
|
||||||
|
|
||||||
|
// Set Header Title
|
||||||
|
let titleText = 'All Mapped Surveillance Corridors';
|
||||||
|
if (q) {
|
||||||
|
titleText = `"${escapeHtml(rawQuery)}"`;
|
||||||
|
} else if (state.selectedState !== 'ALL') {
|
||||||
|
titleText = `${state.selectedState} Corridor Locations`;
|
||||||
|
}
|
||||||
|
el.dirCamQueryTitle.innerHTML = titleText;
|
||||||
|
el.dirCamBadgeCount.textContent = `${matchingCams.length} Camera Nodes Mapped`;
|
||||||
|
|
||||||
|
// Render Camera Cards with Images, Cross-Streets, and Live Actions
|
||||||
|
const cardsHtml = matchingCams.map(cam => {
|
||||||
|
const meta = (window.FlockAuth && window.FlockAuth.getCameraMeta)
|
||||||
|
? window.FlockAuth.getCameraMeta(cam.id)
|
||||||
|
: { confirmations: 1, hasPhoto: false, isContested: false };
|
||||||
|
|
||||||
|
const photoUrl = meta.photoUrl || cam.photoUrl || null;
|
||||||
|
const hasPhoto = !!photoUrl;
|
||||||
|
|
||||||
|
const badgeHtml = meta.isContested
|
||||||
|
? `<span class="dir-cam-badge unverified">🟠 CONTESTED</span>`
|
||||||
|
: (hasPhoto ? `<span class="dir-cam-badge verified">🟢 PHOTO VERIFIED</span>` : `<span class="dir-cam-badge unverified">⚠️ FLOCK RECORDED</span>`);
|
||||||
|
|
||||||
|
const imageContainerHtml = hasPhoto
|
||||||
|
? `<div class="dir-cam-image-container">
|
||||||
|
<img src="${photoUrl}" class="dir-cam-img" alt="Camera at ${escapeHtml(cam.name)}" onclick="window.open('${photoUrl}', '_blank')" title="Click to view full photo proof">
|
||||||
|
${badgeHtml}
|
||||||
|
</div>`
|
||||||
|
: `<div class="dir-cam-image-container">
|
||||||
|
<div class="dir-cam-placeholder">
|
||||||
|
<span class="dir-cam-placeholder-icon">📸</span>
|
||||||
|
<strong style="font-size: 0.82rem; color: #cbd5e1;">Hardware Photo Pending</strong>
|
||||||
|
<span style="font-size: 0.72rem; color: #94a3b8;">Mounted on utility / signal pole</span>
|
||||||
|
</div>
|
||||||
|
${badgeHtml}
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<article class="dir-cam-card" id="dir-cam-node-${escapeHtml(cam.id)}">
|
||||||
|
${imageContainerHtml}
|
||||||
|
<div class="dir-cam-body">
|
||||||
|
<div>
|
||||||
|
<h4 class="dir-cam-intersection">${escapeHtml(cam.name)}</h4>
|
||||||
|
<div class="dir-cam-agency-meta">
|
||||||
|
<span>🏛️ ${escapeHtml(cam.agency || 'Local Law Enforcement')}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dir-cam-specs-row">
|
||||||
|
<div class="dir-cam-spec-item">
|
||||||
|
<span class="dir-cam-spec-label">HARDWARE</span>
|
||||||
|
<span class="dir-cam-spec-val">${escapeHtml(cam.type || 'Falcon Solar ALPR')}</span>
|
||||||
|
</div>
|
||||||
|
<div class="dir-cam-spec-item">
|
||||||
|
<span class="dir-cam-spec-label">MONTHLY SCANS</span>
|
||||||
|
<span class="dir-cam-spec-val" style="color: #34d399;">${escapeHtml(cam.captures30d ? `${cam.captures30d}` : '300,000+')}</span>
|
||||||
|
</div>
|
||||||
|
<div class="dir-cam-spec-item">
|
||||||
|
<span class="dir-cam-spec-label">COORDINATES</span>
|
||||||
|
<span class="dir-cam-spec-val" style="font-family: var(--font-mono); font-size: 0.75rem;">${cam.lat.toFixed(4)}, ${cam.lng.toFixed(4)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="dir-cam-spec-item">
|
||||||
|
<span class="dir-cam-spec-label">CONSENSUS</span>
|
||||||
|
<span class="dir-cam-spec-val" style="color: #38bdf8;">👍 ${meta.confirmations || 1} Confirmed</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dir-cam-actions">
|
||||||
|
<button type="button" class="btn-dir-cam-jump" onclick="window.jumpToRadarCamera('${escapeHtml(cam.id)}')">
|
||||||
|
<span>🗺️</span>
|
||||||
|
<span>View on Live Radar</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn-dir-cam-details" onclick="window.openCameraHardwareProof('${escapeHtml(cam.id)}')">
|
||||||
|
<span>📸</span>
|
||||||
|
<span>Camera Info</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
el.directoryCamerasGrid.innerHTML = cardsHtml;
|
||||||
|
}
|
||||||
|
|
||||||
// --- RENDER MAIN DIRECTORY ---
|
// --- RENDER MAIN DIRECTORY ---
|
||||||
function renderDirectory() {
|
function renderDirectory() {
|
||||||
|
// Render specific camera nodes with photos and cross streets if matching
|
||||||
|
renderDirectoryCameras(state.searchQuery, state.filteredPortals);
|
||||||
|
|
||||||
const total = state.filteredPortals.length;
|
const total = state.filteredPortals.length;
|
||||||
el.totalCount.textContent = state.allPortals.length.toLocaleString();
|
el.totalCount.textContent = state.allPortals.length.toLocaleString();
|
||||||
el.showingCount.textContent = total.toLocaleString();
|
el.showingCount.textContent = total.toLocaleString();
|
||||||
|
|||||||
+18
@@ -269,6 +269,7 @@
|
|||||||
zoomControl: false,
|
zoomControl: false,
|
||||||
attributionControl: false
|
attributionControl: false
|
||||||
});
|
});
|
||||||
|
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', {
|
||||||
@@ -2089,6 +2090,23 @@
|
|||||||
window.openAgencyModal(slug);
|
window.openAgencyModal(slug);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Global camera list provider for Directory integration
|
||||||
|
window.getFlockAllCameras = function() {
|
||||||
|
return [...allCameras];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Global jump from Directory straight to specific camera on Radar
|
||||||
|
window.jumpToRadarCamera = function(camId) {
|
||||||
|
if (el.navMapBtn) el.navMapBtn.click();
|
||||||
|
setTimeout(() => {
|
||||||
|
const cam = allCameras.find(c => c.id === camId);
|
||||||
|
if (cam && map) {
|
||||||
|
map.flyTo([cam.lat, cam.lng], 16, { animate: true, duration: 1.0 });
|
||||||
|
}
|
||||||
|
openCameraHardwareProof(camId);
|
||||||
|
}, 150);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(str) {
|
function escapeHtml(str) {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
// Service Worker for FlockRadar Offline Driving & Live Updates
|
// Service Worker for FlockRadar Offline Driving & Live Updates
|
||||||
const CACHE_NAME = 'flockradar-v10';
|
const CACHE_NAME = 'flockradar-v11';
|
||||||
const ASSETS_TO_CACHE = [
|
const ASSETS_TO_CACHE = [
|
||||||
'./',
|
'./',
|
||||||
'./index.html',
|
'./index.html',
|
||||||
'./css/styles.css?v=10',
|
'./css/styles.css?v=11',
|
||||||
'./js/app.js?v=10',
|
'./js/app.js?v=11',
|
||||||
'./js/auth.js?v=10',
|
'./js/auth.js?v=11',
|
||||||
'./js/radar.js?v=10',
|
'./js/radar.js?v=11',
|
||||||
'./data/portals.js?v=10',
|
'./data/portals.js?v=11',
|
||||||
'./manifest.json'
|
'./manifest.json'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user