stable: finalized GIS aesthetics and Syria/Jordan routing synchronization

This commit is contained in:
Hamza-Ayed
2026-03-28 04:12:01 +03:00
parent d6f3b41b53
commit 592af8ce5c
203 changed files with 9711 additions and 119 deletions
+188 -22
View File
@@ -1,11 +1,101 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import MapComponent from './components/MapComponent';
import { Navigation, Map as MapIcon, Database, Activity, Compass } from 'lucide-react';
import { Navigation, Compass, Activity, BarChart3, MapPin } from 'lucide-react';
import { decodePolyline } from './utils/polyline';
function App() {
const [map, setMap] = useState<any>(null);
const [debug, setDebug] = useState({ zoom: 12, center: [35.91, 31.95], bounds: '' });
const [routeData, setRouteData] = useState<any>(null);
const [stats, setStats] = useState<any>(null);
const [loading, setLoading] = useState(false);
const [show3D, setShow3D] = useState(false);
const [showPOIs, setShowPOIs] = useState(true);
// Geocoding State
const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState<any[]>([]);
const [showResults, setShowResults] = useState(false);
const [newPlace, setNewPlace] = useState<{lat: number, lng: number} | null>(null);
const [placeForm, setPlaceForm] = useState({ name: '', name_ar: '', category: '' });
const handleMapClick = (lat: number, lng: number) => {
setNewPlace({ lat, lng });
};
const handleSearch = async () => {
if (searchQuery.length < 3) return;
setLoading(true);
try {
const apiUrl = (import.meta as any).env.VITE_API_URL || '/api';
let queryUrl = `${apiUrl}/geocoding/search?q=${searchQuery}&radius=20000`;
if (map) {
const center = map.getCenter();
queryUrl += `&lat=${center.lat}&lng=${center.lng}`;
}
const response = await fetch(queryUrl, {
headers: { 'x-api-key': 'intaleq_secret_2026' }
});
const data = await response.json();
setSearchResults(data.results || []);
setShowResults(true);
if (data.results && data.results.length > 0 && map) {
const place = data.results[0];
map.flyTo({ center: [place.location.lng, place.location.lat], zoom: 15 });
}
} catch (e) {
console.error("Search failed", e);
} finally {
setLoading(false);
}
};
const submitNewPlace = async () => {
if (!newPlace) return;
try {
const apiUrl = (import.meta as any).env.VITE_API_URL || '/api';
await fetch(`${apiUrl}/geocoding/places`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': 'intaleq_secret_2026' },
body: JSON.stringify({
latitude: newPlace.lat,
longitude: newPlace.lng,
name: placeForm.name,
name_ar: placeForm.name_ar,
category: placeForm.category,
})
});
setNewPlace(null);
setPlaceForm({ name: '', name_ar: '', category: '' });
alert('Place added successfully! / تم إضافة المكان بنجاح');
} catch (e) {
console.error(e);
alert('Failed to add place / فشل إضافة المكان');
}
};
const fetchStats = async () => {
try {
const apiUrl = (import.meta as any).env.VITE_API_URL || '/api';
const response = await fetch(`${apiUrl}/map-refinement/summary`);
if (!response.ok) throw new Error('Stats fetch failed');
const data = await response.json();
setStats(data);
} catch (error) {
console.warn("Stats fetch failed, using fallback UI");
}
};
useEffect(() => {
fetchStats();
const interval = setInterval(fetchStats, 60000);
return () => clearInterval(interval);
}, []);
const handleMapLoad = (initializedMap: any) => {
setMap(initializedMap);
@@ -19,8 +109,6 @@ function App() {
});
};
const [loading, setLoading] = useState(false);
const calculateRoute = async () => {
setLoading(true);
try {
@@ -28,20 +116,23 @@ function App() {
const from = [31.9539, 35.9106];
const to = [32.0608, 36.1032];
const response = await fetch(`http://localhost:3200/maps/route?fromLat=${from[0]}&fromLng=${from[1]}&toLat=${to[0]}&toLng=${to[1]}`);
const apiUrl = (import.meta as any).env.VITE_API_URL || '/api';
const apiKey = (import.meta as any).env.VITE_API_KEY || 'intaleq_secret_2026';
const response = await fetch(`${apiUrl}/maps/route?fromLat=${from[0]}&fromLng=${from[1]}&toLat=${to[0]}&toLng=${to[1]}`, {
headers: { 'x-api-key': apiKey }
});
const data = await response.json();
if (data.points && map) {
setRouteData(data);
// Draw on map
const coords = typeof data.points === 'string' ? decodePolyline(data.points) : data.points;
setRouteData({ ...data, points: coords });
if (map.getSource('route')) {
map.getSource('route').setData({
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates: data.points
}
geometry: { type: 'LineString', coordinates: coords }
});
}
}
@@ -52,16 +143,49 @@ function App() {
}
};
const [show3D, setShow3D] = useState(false);
const [showPOIs, setShowPOIs] = useState(true);
return (
<div className="app">
{/* Sidebar - لوحة التحكم */}
<div className="sidebar glass-morphism">
<h1>Jordan Maps SaaS</h1>
<p style={{ color: 'var(--text-muted)', fontSize: '0.8rem' }}>Self-Hosted Mobility Prototype</p>
<div className="input-group">
<label><MapPin size={14} style={{ marginRight: 5 }} /> Search / البحث</label>
<div style={{ display: 'flex', gap: '5px' }}>
<input type="text" placeholder="Coffee shop..." value={searchQuery} onChange={e => setSearchQuery(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleSearch()} />
<button className="btn" style={{ width: 'auto', marginTop: 0, padding: '10px 15px' }} onClick={handleSearch}>Go</button>
</div>
{showResults && searchResults.length > 0 && (
<div className="search-results-list" style={{ marginTop: '10px', maxHeight: '200px', overflowY: 'auto', background: 'rgba(255,255,255,0.05)', borderRadius: '8px' }}>
{searchResults.map((res) => (
<div
key={res.id}
onClick={() => map?.flyTo({ center: [res.location.lng, res.location.lat], zoom: 16 })}
style={{ padding: '8px', borderBottom: '1px solid var(--glass-border)', cursor: 'pointer', fontSize: '0.85rem' }}
className="search-result-item"
>
<div style={{ fontWeight: 600 }}>{res.name_ar || res.name}</div>
{res.address && <div style={{ fontSize: '0.75rem', opacity: 0.7 }}>{res.address}</div>}
<div style={{ fontSize: '0.7rem', color: '#3b82f6', marginTop: '2px' }}>
{res.distance_km} km away | {res.source.replace('_', ' ')}
</div>
</div>
))}
<button
className="btn-link"
style={{ width: '100%', padding: '5px', fontSize: '0.7rem', background: 'transparent', border: 'none', color: 'var(--text-muted)' }}
onClick={() => setShowResults(false)}
>
Clear Results / مسح
</button>
</div>
)}
{showResults && searchResults.length === 0 && (
<div style={{ marginTop: '10px', fontSize: '0.8rem', color: '#ef4444' }}>No results found near you.</div>
)}
</div>
<div className="input-group">
<label><Navigation size={14} style={{ marginRight: 5 }} /> Origin / نقطة الانطلاق</label>
<input type="text" placeholder="Amman, Jordan" defaultValue="31.9539, 35.9106" />
@@ -72,16 +196,14 @@ function App() {
<input type="text" placeholder="Zarqa, Jordan" defaultValue="32.0608, 36.1032" />
</div>
<button className="btn" onClick={calculateRoute}>
Calculate Route / حساب المسار
<button className="btn" onClick={calculateRoute} disabled={loading}>
{loading ? 'Calculating...' : 'Calculate Route / حساب المسار'}
</button>
<hr style={{ border: 'none', borderTop: '1px solid var(--glass-border)', margin: '10px 0' }} />
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h3 style={{ margin: 0 }}>Options / خيارات</h3>
</div>
<h3>Options / خيارات</h3>
<div className="toggle-group" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<label style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px' }}>
@@ -110,15 +232,59 @@ function App() {
</div>
</div>
{/* Map - الخريطة */}
<div className="map-container">
<MapComponent
onMapLoad={handleMapLoad}
onMapClick={handleMapClick}
show3D={show3D}
showPOIs={showPOIs}
/>
{/* Add Place Modal */}
{newPlace && (
<div className="stats-panel glass-morphism" style={{ top: '50%', left: '50%', transform: 'translate(-50%, -50%)', zIndex: 1000, width: '300px' }}>
<h4>Add New Place / إضافة مكان</h4>
<div className="input-group">
<label>Name / الاسم</label>
<input type="text" value={placeForm.name} onChange={e => setPlaceForm({...placeForm, name: e.target.value})} />
</div>
<div className="input-group">
<label>Arabic Name / الاسم بالعربي</label>
<input type="text" value={placeForm.name_ar} onChange={e => setPlaceForm({...placeForm, name_ar: e.target.value})} />
</div>
<div className="input-group">
<label>Category / التصنيف</label>
<input type="text" value={placeForm.category} onChange={e => setPlaceForm({...placeForm, category: e.target.value})} />
</div>
<div style={{ display: 'flex', gap: '10px', marginTop: '10px' }}>
<button className="btn" style={{ flex: 1 }} onClick={submitNewPlace}>Save</button>
<button className="btn" style={{ flex: 1, background: '#475569' }} onClick={() => setNewPlace(null)}>Cancel</button>
</div>
</div>
)}
{stats && stats.telemetry && (
<div className="stats-panel glass-morphism">
<h4><BarChart3 size={16} style={{ verticalAlign: 'middle', marginRight: '8px' }} /> Map Insights / التحليل</h4>
<div className="stat-item">
<span className="stat-label">Total GPS Points / إجمالي النقاط</span>
<span className="stat-value">{stats.telemetry?.totalPoints?.toLocaleString()}</span>
</div>
<div className="stat-item">
<span className="stat-label">Last 24h / آخر 24 ساعة</span>
<span className="stat-badge">+{stats.telemetry?.last24h?.toLocaleString()}</span>
</div>
<div className="stat-item">
<span className="stat-label">Updated Roads / شوارع محدثة</span>
<span className="stat-value">{stats.roadSegments?.analyzed}</span>
</div>
<div className="stat-item">
<span className="stat-label">Candidate Roads / شـوارع مرشحة</span>
<span className="stat-value">{stats.candidateRoads?.total}</span>
</div>
</div>
)}
{/* Debug Panel - لوحة المعلومات */}
<div className="debug-panel glass-morphism">
<div>Zoom: {debug.zoom}</div>
<div>Center: {debug.center[0]}, {debug.center[1]}</div>