feat: add Soviet Military Topo and Google Topo, remove HERE API dependency, relocate and restyle floating ViewNav

This commit is contained in:
Hamza-Ayed
2026-08-16 14:09:02 +03:00
parent 126761004c
commit 844774bd1b
3 changed files with 91 additions and 101 deletions
+35 -6
View File
@@ -15,21 +15,50 @@ maplibregl.setRTLTextPlugin(
// Lightweight hash router (no dependency). '#compare' and '#review' are additive
// views; anything else falls back to the existing map app, untouched.
const NAV = [
{ hash: '#map', label: 'Map' },
{ hash: '#review', label: 'Review' },
{ hash: '#compare', label: 'Compare' },
{ hash: '#map', label: 'Map', icon: '🗺️' },
{ hash: '#review', label: 'Review', icon: '🔍' },
{ hash: '#compare', label: 'Compare', icon: '⚖️' },
]
function ViewNav({ hash }: { hash: string }) {
const isMap = hash !== '#compare' && hash !== '#review'
return (
<div style={{ position: 'fixed', top: 8, left: '50%', transform: 'translateX(-50%)', zIndex: 9999, display: 'flex', gap: 4, background: 'rgba(15,23,42,0.9)', border: '1px solid #334155', borderRadius: 999, padding: 3, fontFamily: 'Inter, system-ui, sans-serif' }}>
<div style={{
position: 'fixed',
top: 10,
right: 16,
zIndex: 9999,
display: 'flex',
alignItems: 'center',
gap: 4,
background: 'rgba(15, 23, 42, 0.88)',
backdropFilter: 'blur(12px)',
border: '1px solid rgba(255, 255, 255, 0.12)',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.45)',
borderRadius: 999,
padding: '3px 4px',
fontFamily: 'Inter, system-ui, sans-serif'
}}>
{NAV.map(n => {
const active = n.hash === '#map' ? isMap : hash === n.hash
return (
<a key={n.hash} href={n.hash}
style={{ textDecoration: 'none', fontSize: 12, fontWeight: 600, color: active ? '#fff' : '#94a3b8', background: active ? '#6366f1' : 'transparent', padding: '5px 12px', borderRadius: 999 }}>
{n.label}
style={{
textDecoration: 'none',
fontSize: 12,
fontWeight: 600,
color: active ? '#fff' : '#94a3b8',
background: active ? 'linear-gradient(135deg, #6366f1, #4f46e5)' : 'transparent',
boxShadow: active ? '0 2px 8px rgba(99, 102, 241, 0.4)' : 'none',
padding: '5px 13px',
borderRadius: 999,
display: 'flex',
alignItems: 'center',
gap: 5,
transition: 'all 0.15s ease'
}}>
<span>{n.icon}</span>
<span>{n.label}</span>
</a>
)
})}
+30 -79
View File
@@ -21,20 +21,32 @@ import { attachIconLoader } from '../utils/mapIcons';
// absolute — an empty default resolved to the app host, which serves no tiles.
const TILES = (import.meta as any).env.VITE_TILES_URL || 'https://tiles.intaleqapp.com';
type RefKey = 'google-hybrid' | 'google-sat' | 'google-streets' | 'esri-sat' | 'esri-clarity' | 'esri-streets' | 'esri-topo' | 'osm' | 'here-sat' | 'here-streets';
type RefKey =
| 'google-hybrid'
| 'google-topo'
| 'soviet-military'
| 'opentopo'
| 'esri-clarity'
| 'google-sat'
| 'google-streets'
| 'esri-sat'
| 'esri-topo'
| 'esri-streets'
| 'osm';
const getRefs = (hereKey: string): Record<RefKey, { label: string; tiles: string; attribution: string; maxzoom: number }> => ({
'google-hybrid': { label: '🛰️ Google Hybrid', tiles: 'https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
'google-sat': { label: '🛰️ Google Sat', tiles: 'https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
'google-streets': { label: '🗺️ Google Streets', tiles: 'https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
'esri-sat': { label: '🛰️ Esri Satellite', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri, Maxar', maxzoom: 19 },
'esri-clarity': { label: '✨ Esri Clarity', tiles: 'https://clarity.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
'esri-streets': { label: '🗺️ Esri Streets', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
'esri-topo': { label: '🏔️ Esri Topo', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
'here-sat': { label: '🛰️ HERE Satellite', tiles: `https://maps.hereapi.com/v3/base/mc/{z}/{x}/{y}/jpeg?style=hybrid.day&apiKey=${hereKey || 'YOUR_HERE_API_KEY'}`, attribution: '© HERE Technologies', maxzoom: 19 },
'here-streets': { label: '🗺️ HERE Streets', tiles: `https://maps.hereapi.com/v3/base/mc/{z}/{x}/{y}/png8?style=explore.day&apiKey=${hereKey || 'YOUR_HERE_API_KEY'}`, attribution: '© HERE Technologies', maxzoom: 19 },
'osm': { label: '🧭 OSM Standard', tiles: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', attribution: '© OpenStreetMap', maxzoom: 19 },
});
const REFS: Record<RefKey, { label: string; tiles: string; attribution: string; maxzoom: number }> = {
'google-hybrid': { label: '🛰️ Google Hybrid', tiles: 'https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
'google-topo': { label: '🏔️ Google Topo', tiles: 'https://mt1.google.com/vt/lyrs=p&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
'soviet-military': { label: '🇷🇺 Soviet Military Topo', tiles: 'https://maps.marshruty.ru/ml.ashx?x={x}&y={y}&z={z}&i=1&al=1', attribution: '© Soviet Military Topo 1:100k', maxzoom: 15 },
'opentopo': { label: '🧭 OpenTopoMap', tiles: 'https://tile.opentopomap.org/{z}/{x}/{y}.png', attribution: '© OpenTopoMap', maxzoom: 17 },
'esri-clarity': { label: '✨ Esri Clarity HD', tiles: 'https://clarity.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
'google-sat': { label: '🛰️ Google Sat', tiles: 'https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
'google-streets': { label: '🗺️ Google Streets', tiles: 'https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
'esri-sat': { label: '🛰️ Esri Satellite', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri, Maxar', maxzoom: 19 },
'esri-topo': { label: '🏔️ Esri Topo', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
'esri-streets': { label: '🗺️ Esri Streets', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
'osm': { label: '🧭 OSM Standard', tiles: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', attribution: '© OpenStreetMap', maxzoom: 19 },
};
const rasterStyle = (ref: { tiles: string; attribution: string; maxzoom: number }): any => ({
version: 8,
@@ -49,12 +61,6 @@ const CompareView: React.FC = () => {
const rightMap = useRef<maplibregl.Map | null>(null);
const syncing = useRef(false);
const [hereApiKey, setHereApiKey] = useState<string>(() => {
return localStorage.getItem('here_api_key') || (import.meta as any).env.VITE_HERE_API_KEY || '';
});
const [showKeyModal, setShowKeyModal] = useState(false);
const [tempKey, setTempKey] = useState('');
const [refKey, setRefKey] = useState<RefKey>('google-hybrid');
const [center, setCenter] = useState<{ lng: number; lat: number; zoom: number }>({ lng: 35.91, lat: 31.95, zoom: 15 });
@@ -125,75 +131,20 @@ const CompareView: React.FC = () => {
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh', background: '#0f172a', color: '#e2e8f0', fontFamily: 'Inter, system-ui, sans-serif' }}>
{/* Toolbar */}
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', padding: '10px 16px', borderBottom: '1px solid #1e293b', flexWrap: 'wrap' }}>
<a href="#" style={{ ...btn, borderColor: '#6366f1', color: '#a5b4fc' }}>← Map</a>
<strong style={{ fontSize: '0.9rem' }}>Compare</strong>
<span style={{ color: '#64748b', fontSize: '0.78rem' }}>Our map ⟷ reference — synced</span>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', padding: '10px 16px', paddingRight: '280px', borderBottom: '1px solid #1e293b', background: '#0f172a' }}>
<strong style={{ fontSize: '0.9rem', whiteSpace: 'nowrap' }}>⚖️ Compare</strong>
<span style={{ color: '#64748b', fontSize: '0.78rem', whiteSpace: 'nowrap' }}>Our map ⟷ reference — synced</span>
<div style={{ display: 'flex', gap: '6px', marginInlineStart: 'auto', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', gap: '6px', marginInlineStart: 'auto', overflowX: 'auto', paddingBottom: '2px' }}>
{(Object.keys(REFS) as RefKey[]).map(k => (
<button key={k} onClick={() => setRefKey(k)}
style={{ ...btn, ...(refKey === k ? { borderColor: '#6366f1', color: '#fff', background: 'rgba(99,102,241,0.2)' } : {}) }}>
style={{ ...btn, whiteSpace: 'nowrap', flexShrink: 0, ...(refKey === k ? { borderColor: '#6366f1', color: '#fff', background: 'rgba(99,102,241,0.35)', fontWeight: 600 } : {}) }}>
{REFS[k].label}
</button>
))}
{refKey.startsWith('here') && (
<button
onClick={() => {
setTempKey(hereApiKey);
setShowKeyModal(true);
}}
style={{ ...btn, borderColor: '#f59e0b', color: '#fcd34d', background: 'rgba(245,158,11,0.1)' }}
title="Configure HERE API Key"
>
🔑 {hereApiKey ? 'HERE Key Configured' : 'Set HERE Key'}
</button>
)}
</div>
</div>
{/* HERE API Key Modal */}
{showKeyModal && (
<div style={{ position: 'fixed', inset: 0, zIndex: 999, background: 'rgba(0,0,0,0.7)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ background: '#1e293b', padding: '20px', borderRadius: '12px', width: '90%', maxWidth: '440px', border: '1px solid #334155', display: 'flex', flexDirection: 'column', gap: '12px' }}>
<h3 style={{ margin: 0, fontSize: '1rem', color: '#f8fafc' }}>🔑 HERE Platform API Key</h3>
<p style={{ margin: 0, fontSize: '0.8rem', color: '#94a3b8', lineHeight: 1.4 }}>
Enter your official HERE Raster Tile API v3 key to view official HERE Satellite & Streets imagery.
</p>
<input
type="text"
placeholder="Paste your HERE apiKey here..."
value={tempKey}
onChange={(e) => setTempKey(e.target.value)}
style={{ background: '#0f172a', border: '1px solid #475569', borderRadius: '6px', padding: '8px 12px', color: '#fff', fontSize: '0.85rem', width: '100%', boxSizing: 'border-box' }}
/>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '6px' }}>
<button
onClick={() => setShowKeyModal(false)}
style={{ ...btn, background: 'transparent', borderColor: '#475569' }}
>
Cancel
</button>
<button
onClick={() => {
const clean = tempKey.trim();
setHereApiKey(clean);
if (clean) {
localStorage.setItem('here_api_key', clean);
} else {
localStorage.removeItem('here_api_key');
}
setShowKeyModal(false);
}}
style={{ ...btn, background: '#6366f1', color: '#fff', borderColor: '#6366f1', fontWeight: 600 }}
>
Save Key
</button>
</div>
</div>
</div>
)}
{/* Maps */}
<div style={{ position: 'relative', flex: 1, display: 'flex' }}>
<div style={{ position: 'relative', flex: 1, borderInlineEnd: '2px solid #0f172a' }}>
+26 -16
View File
@@ -5,20 +5,32 @@ import { Activity, Map as MapIcon, Settings, ShieldAlert, CheckCircle2, XCircle,
interface CandidateRoad { id: string; uniqueDriverCount: number; totalPoints: number; lengthMeters: number; confidence: number; status: string; source?: string; name?: string; highway?: string; geometry?: any; }
interface Closure { segmentId: string; sampleCount: number; lastUpdated: string; }
type RefKey = 'google-hybrid' | 'google-sat' | 'google-streets' | 'esri-sat' | 'esri-clarity' | 'esri-streets' | 'esri-topo' | 'osm' | 'here-sat' | 'here-streets';
type RefKey =
| 'google-hybrid'
| 'google-topo'
| 'soviet-military'
| 'opentopo'
| 'esri-clarity'
| 'google-sat'
| 'google-streets'
| 'esri-sat'
| 'esri-topo'
| 'esri-streets'
| 'osm';
const getRefs = (hereKey: string): Record<RefKey, { label: string; tiles: string; attribution: string; maxzoom: number }> => ({
'google-hybrid': { label: '🛰️ Google Hybrid', tiles: 'https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
'google-sat': { label: '🛰️ Google Sat', tiles: 'https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
'google-streets': { label: '🗺️ Google Streets', tiles: 'https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
'esri-sat': { label: '🛰️ Esri Satellite', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri, Maxar', maxzoom: 19 },
'esri-clarity': { label: '✨ Esri Clarity', tiles: 'https://clarity.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
'esri-streets': { label: '🗺️ Esri Streets', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
'esri-topo': { label: '🏔️ Esri Topo', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
'here-sat': { label: '🛰️ HERE Satellite', tiles: `https://maps.hereapi.com/v3/base/mc/{z}/{x}/{y}/jpeg?style=hybrid.day&apiKey=${hereKey || 'YOUR_HERE_API_KEY'}`, attribution: '© HERE', maxzoom: 19 },
'here-streets': { label: '🗺️ HERE Streets', tiles: `https://maps.hereapi.com/v3/base/mc/{z}/{x}/{y}/png8?style=explore.day&apiKey=${hereKey || 'YOUR_HERE_API_KEY'}`, attribution: '© HERE', maxzoom: 19 },
'osm': { label: '🧭 OSM Standard', tiles: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', attribution: '© OpenStreetMap', maxzoom: 19 },
});
const REFS: Record<RefKey, { label: string; tiles: string; attribution: string; maxzoom: number }> = {
'google-hybrid': { label: '🛰️ Google Hybrid', tiles: 'https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
'google-topo': { label: '🏔️ Google Topo', tiles: 'https://mt1.google.com/vt/lyrs=p&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
'soviet-military': { label: '🇷🇺 Soviet Military Topo', tiles: 'https://maps.marshruty.ru/ml.ashx?x={x}&y={y}&z={z}&i=1&al=1', attribution: '© Soviet Military Topo 1:100k', maxzoom: 15 },
'opentopo': { label: '🧭 OpenTopoMap', tiles: 'https://tile.opentopomap.org/{z}/{x}/{y}.png', attribution: '© OpenTopoMap', maxzoom: 17 },
'esri-clarity': { label: '✨ Esri Clarity HD', tiles: 'https://clarity.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
'google-sat': { label: '🛰️ Google Sat', tiles: 'https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
'google-streets': { label: '🗺️ Google Streets', tiles: 'https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}', attribution: '© Google', maxzoom: 20 },
'esri-sat': { label: '🛰️ Esri Satellite', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri, Maxar', maxzoom: 19 },
'esri-topo': { label: '🏔️ Esri Topo', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
'esri-streets': { label: '🗺️ Esri Streets', tiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', attribution: '© Esri', maxzoom: 19 },
'osm': { label: '🧭 OSM Standard', tiles: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', attribution: '© OpenStreetMap', maxzoom: 19 },
};
const API = (import.meta as any).env.VITE_API_URL || '/api';
@@ -39,8 +51,6 @@ const IntelligenceDashboard: React.FC = () => {
const [loading, setLoading] = useState(true);
const [apiKey, setApiKey] = useState(localStorage.getItem('map_admin_key') || '');
const [needsAuth, setNeedsAuth] = useState(!localStorage.getItem('map_admin_key'));
const [hereApiKey, setHereApiKey] = useState(localStorage.getItem('here_api_key') || '');
const REFS = getRefs(hereApiKey);
// Right Map State
const [refKey, setRefKey] = useState<RefKey>('google-hybrid');
@@ -587,7 +597,7 @@ const IntelligenceDashboard: React.FC = () => {
<div style={{ flex: 1, position: 'relative' }}>
<div ref={rightDiv} style={{ width: '100%', height: '100%' }} />
<div style={{ position: 'absolute', top: 10, left: 10, right: 10, zIndex: 2, display: 'flex', gap: '6px', overflowX: 'auto', paddingBottom: '4px', maxWidth: 'calc(100% - 20px)' }}>
<div style={{ position: 'absolute', top: 10, left: 10, right: 280, zIndex: 2, display: 'flex', gap: '6px', overflowX: 'auto', paddingBottom: '4px' }}>
{(Object.keys(REFS) as Array<keyof typeof REFS>).map(k => (
<button key={k} onClick={() => setRefKey(k)} style={{ background: refKey === k ? 'rgba(99,102,241,0.95)' : 'rgba(15,23,42,0.85)', border: '1px solid #334155', color: refKey === k ? 'white' : '#e2e8f0', padding: '4px 10px', borderRadius: '20px', fontSize: '0.72rem', fontWeight: 600, cursor: 'pointer', whiteSpace: 'nowrap', flexShrink: 0 }}>
{REFS[k].label}