740 lines
29 KiB
HTML
740 lines
29 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ar" dir="rtl">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Intaleq Premium Map V3 - خرائط انطلاقة الذكية</title>
|
||
|
||
<link href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" rel="stylesheet" />
|
||
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
|
||
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;800&family=Noto+Sans+Arabic:wght@400;700&display=swap');
|
||
|
||
:root {
|
||
--primary: #c0a048;
|
||
--primary-dark: #8a6e20;
|
||
--accent: #2563eb;
|
||
--bg-glass: rgba(255, 255, 255, 0.88);
|
||
--shadow: 0 12px 40px rgba(0,0,0,0.12);
|
||
}
|
||
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
|
||
body {
|
||
font-family: 'Outfit', 'Noto Sans Arabic', sans-serif;
|
||
background: #F2EDE4;
|
||
overflow: hidden;
|
||
color: #342C20;
|
||
}
|
||
|
||
/* ── MAP ── */
|
||
#map { width: 100vw; height: 100vh; position: absolute; inset: 0; }
|
||
|
||
/* ── LOADING OVERLAY ── */
|
||
#loading {
|
||
display: none; position: fixed; inset: 0;
|
||
background: rgba(242,237,228,0.6);
|
||
z-index: 2000; align-items: center; justify-content: center;
|
||
backdrop-filter: blur(6px);
|
||
}
|
||
.spinner {
|
||
width: 40px; height: 40px;
|
||
border: 4px solid rgba(192,160,72,0.15);
|
||
border-top: 4px solid var(--primary);
|
||
border-radius: 50%;
|
||
animation: spin 0.9s linear infinite;
|
||
}
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
|
||
/* ── CONTROLS PANEL ── */
|
||
.controls {
|
||
position: absolute; top: 24px; right: 24px; z-index: 100;
|
||
background: var(--bg-glass);
|
||
backdrop-filter: blur(14px) saturate(180%);
|
||
-webkit-backdrop-filter: blur(14px) saturate(180%);
|
||
border-radius: 24px; padding: 28px; width: 380px;
|
||
box-shadow: var(--shadow);
|
||
border: 1px solid rgba(255,255,255,0.35);
|
||
}
|
||
|
||
.badge {
|
||
display: inline-block; padding: 4px 10px; border-radius: 6px;
|
||
font-size: 11px; font-weight: 800;
|
||
background: var(--primary); color: #fff;
|
||
margin-bottom: 12px; text-transform: uppercase; letter-spacing: 1px;
|
||
}
|
||
|
||
.panel-title { font-size: 22px; font-weight: 800; margin-bottom: 6px; color: #18100A; }
|
||
.panel-desc { font-size: 13px; color: #584840; margin-bottom: 20px; line-height: 1.55; }
|
||
|
||
/* ── SEARCH ── */
|
||
.search-wrap { position: relative; margin-bottom: 20px; }
|
||
.search-wrap input {
|
||
width: 100%; padding: 13px 60px 13px 16px;
|
||
border-radius: 12px; border: 1.5px solid #ddd;
|
||
font-family: inherit; font-size: 14px;
|
||
outline: none; transition: border-color 0.25s;
|
||
background: rgba(255,255,255,0.7);
|
||
}
|
||
.search-wrap input:focus { border-color: var(--primary); }
|
||
.search-wrap .search-btn {
|
||
position: absolute; left: 8px; top: 50%; transform: translateY(-50%);
|
||
background: var(--primary); color: #fff;
|
||
border: none; padding: 6px 13px; border-radius: 8px;
|
||
cursor: pointer; font-size: 12px; font-weight: 700;
|
||
transition: background 0.2s;
|
||
}
|
||
.search-wrap .search-btn:hover { background: var(--primary-dark); }
|
||
|
||
#search-results {
|
||
display: none; max-height: 220px; overflow-y: auto;
|
||
background: #fff; border-radius: 12px;
|
||
border: 1px solid #eee;
|
||
box-shadow: 0 4px 14px rgba(0,0,0,0.06);
|
||
margin-top: 8px;
|
||
}
|
||
.result-item {
|
||
padding: 11px 16px; border-bottom: 1px solid #f5f5f5;
|
||
cursor: pointer; transition: background 0.15s;
|
||
}
|
||
.result-item:last-child { border-bottom: none; }
|
||
.result-item:hover { background: #fdfaf5; }
|
||
.result-item .name { font-weight: 700; font-size: 13px; }
|
||
.result-item .meta { font-size: 11px; color: #999; margin-top: 2px; }
|
||
|
||
/* ── ROUTE BUTTONS ── */
|
||
.btn-group { display: flex; flex-direction: column; gap: 10px; }
|
||
|
||
.btn {
|
||
padding: 15px 18px; border: none; border-radius: 14px;
|
||
font-size: 14px; font-weight: 700; cursor: pointer;
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
transition: transform 0.25s, box-shadow 0.25s;
|
||
font-family: inherit;
|
||
}
|
||
.btn .label-sub { font-size: 11px; font-weight: 400; opacity: 0.75; margin-bottom: 2px; }
|
||
.btn .icon { font-size: 20px; }
|
||
|
||
.btn-gold {
|
||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
|
||
color: #fff;
|
||
box-shadow: 0 4px 16px rgba(192,160,72,0.28);
|
||
}
|
||
.btn-gold:hover { transform: translateY(-2px); box-shadow: 0 8px 22px rgba(192,160,72,0.38); }
|
||
|
||
.btn-white {
|
||
background: #fff; color: #342C20;
|
||
border: 1px solid rgba(0,0,0,0.06);
|
||
box-shadow: 0 3px 8px rgba(0,0,0,0.04);
|
||
}
|
||
.btn-white:hover { background: #fdfaf5; transform: translateY(-2px); }
|
||
|
||
/* ── STATUS BOX ── */
|
||
#status {
|
||
display: none; margin-top: 20px;
|
||
background: rgba(253,250,245,0.7);
|
||
border: 1.5px dashed var(--primary);
|
||
border-radius: 14px; padding: 16px; font-size: 13px;
|
||
line-height: 1.65;
|
||
animation: fadeUp 0.35s ease-out;
|
||
}
|
||
.status-row {
|
||
display: flex; justify-content: space-between; margin-top: 6px;
|
||
}
|
||
.status-row span:last-child { font-weight: 800; }
|
||
|
||
/* ── DB NAMES INDICATOR ── */
|
||
#db-status {
|
||
margin-top: 14px; padding: 10px 14px;
|
||
background: rgba(192,160,72,0.08);
|
||
border-radius: 10px; font-size: 12px; color: #7A5C10;
|
||
display: flex; align-items: center; gap: 8px;
|
||
}
|
||
.dot-gold {
|
||
width: 8px; height: 8px; border-radius: 50%;
|
||
background: var(--primary); flex-shrink: 0;
|
||
animation: pulse 2s ease-in-out infinite;
|
||
}
|
||
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
|
||
|
||
@keyframes fadeUp {
|
||
from { opacity: 0; transform: translateY(8px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
/* ── BRANDING ── */
|
||
.branding {
|
||
position: absolute; bottom: 28px; left: 28px; z-index: 100;
|
||
background: var(--bg-glass); backdrop-filter: blur(8px);
|
||
padding: 10px 18px; border-radius: 12px;
|
||
display: flex; align-items: center; gap: 10px;
|
||
box-shadow: 0 6px 20px rgba(0,0,0,0.09);
|
||
border: 1px solid rgba(255,255,255,0.4);
|
||
}
|
||
.branding span { font-size: 12px; font-weight: 800; color: #18100A; letter-spacing: 0.5px; }
|
||
|
||
/* ── CONTEXT MENU ── */
|
||
#context-menu {
|
||
display: none; position: fixed; z-index: 3000;
|
||
background: var(--bg-glass); backdrop-filter: blur(12px);
|
||
border-radius: 12px; min-width: 180px;
|
||
box-shadow: 0 8px 30px rgba(0,0,0,0.15);
|
||
border: 1px solid rgba(255,255,255,0.4);
|
||
padding: 6px;
|
||
}
|
||
.menu-item {
|
||
padding: 10px 14px; border-radius: 8px; font-size: 13px; font-weight: 700;
|
||
cursor: pointer; display: flex; align-items: center; gap: 10px;
|
||
transition: background 0.2s;
|
||
}
|
||
.menu-item:hover { background: rgba(192,160,72,0.12); color: var(--primary-dark); }
|
||
.menu-item .icon { font-size: 16px; }
|
||
|
||
/* ── ADMIN PANEL ── */
|
||
#admin-panel {
|
||
position: absolute; bottom: 84px; right: 24px; z-index: 100;
|
||
background: var(--bg-glass); backdrop-filter: blur(14px);
|
||
border-radius: 20px; padding: 20px; width: 340px;
|
||
box-shadow: var(--shadow); border: 1px solid rgba(255,255,255,0.3);
|
||
display: none; animation: fadeUp 0.3s;
|
||
}
|
||
.admin-title { font-size: 16px; font-weight: 800; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
|
||
.admin-close { cursor: pointer; opacity: 0.5; margin-right: auto; font-size: 18px; }
|
||
|
||
.delete-group { margin-top: 15px; }
|
||
.delete-group label { display: block; font-size: 11px; font-weight: 700; margin-bottom: 6px; opacity: 0.7; }
|
||
.delete-group input, .delete-group select {
|
||
width: 100%; padding: 10px; border-radius: 8px; border: 1.5px solid #eee;
|
||
font-size: 13px; margin-bottom: 10px; outline: none;
|
||
}
|
||
.delete-group input:focus { border-color: #c0392b; }
|
||
.btn-delete {
|
||
width: 100%; padding: 10px; background: #c0392b; color: #fff; border: none;
|
||
border-radius: 10px; font-weight: 800; cursor: pointer;
|
||
}
|
||
|
||
#toggle-admin {
|
||
position: absolute; top: 110px; left: 10px; z-index: 100;
|
||
background: var(--bg-glass); border: none; border-radius: 50%;
|
||
width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;
|
||
box-shadow: var(--shadow); cursor: pointer; font-size: 20px;
|
||
}
|
||
|
||
/* ── SCROLLBAR ── */
|
||
::-webkit-scrollbar { width: 5px; }
|
||
::-webkit-scrollbar-track { background: transparent; }
|
||
::-webkit-scrollbar-thumb { background: var(--primary); border-radius: 10px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- MAP -->
|
||
<div id="map"></div>
|
||
|
||
<!-- LOADING -->
|
||
<div id="loading">
|
||
<div style="text-align:center">
|
||
<div class="spinner" style="margin: 0 auto 14px;"></div>
|
||
<strong style="color:var(--primary-dark)">جاري حساب المسار الذكي...</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- BRANDING -->
|
||
<div class="branding">
|
||
<img src="/intaleq-logo.png" alt="Logo" style="height:22px" onerror="this.style.display='none'">
|
||
<span>INTALEQ PREMIUM MAPS</span>
|
||
</div>
|
||
|
||
<!-- CONTROLS -->
|
||
<div class="controls">
|
||
<div class="badge">v3.1.0</div>
|
||
<div class="panel-title">🗺️ خرائط انطلاقة الذكية</div>
|
||
<p class="panel-desc">حلول جغرافية متقدمة لمنطقة الشرق الأوسط وشمال أفريقيا — عرض ثلاثي الأبعاد وأسماء مباشرة من قاعدة البيانات.</p>
|
||
|
||
<!-- SEARCH -->
|
||
<div class="search-wrap">
|
||
<input id="search-input" type="text" placeholder="ابحث عن مكان... (مسجد، مستشفى، ...)" />
|
||
<button class="search-btn" onclick="performSearch()">بحث</button>
|
||
<div id="search-results"></div>
|
||
</div>
|
||
|
||
<!-- ROUTE BUTTONS -->
|
||
<div class="btn-group">
|
||
<button class="btn btn-gold" onclick="handleRoute('LEVANT')">
|
||
<div>
|
||
<div class="label-sub">مسار مقترح — سوريا</div>
|
||
<span>عمّان ← دمشق</span>
|
||
</div>
|
||
<span class="icon">🚀</span>
|
||
</button>
|
||
|
||
<button class="btn btn-white" onclick="handleRoute('EGYPT')">
|
||
<div>
|
||
<div class="label-sub">مسار مقترح — مصر</div>
|
||
<span>شبرا ← الزمالك</span>
|
||
</div>
|
||
<span class="icon">🇪🇬</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- STATUS -->
|
||
<div id="status"></div>
|
||
|
||
<!-- OPTIONS -->
|
||
<div style="margin-top: 24px; border-top: 1px solid rgba(0,0,0,0.06); padding-top: 20px;">
|
||
<div style="font-size: 14px; font-weight: 800; margin-bottom: 15px; color: #18100A;">🛡️ خيارات / Options</div>
|
||
|
||
<label style="display: flex; align-items: center; gap: 10px; margin-bottom: 12px; cursor: pointer; font-size: 13px;">
|
||
<input type="checkbox" id="toggle-3d" checked onchange="updateLayers()" style="accent-color: var(--primary); width: 16px; height: 16px;">
|
||
<span>مباني ثلاثية الأبعاد / 3D Buildings</span>
|
||
</label>
|
||
|
||
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; font-size: 13px;">
|
||
<input type="checkbox" id="toggle-pois" checked onchange="updateLayers()" style="accent-color: var(--primary); width: 16px; height: 16px;">
|
||
<span>عرض المعالم / Show POIs</span>
|
||
</label>
|
||
|
||
<label style="display: flex; align-items: center; gap: 10px; margin-top: 12px; cursor: pointer; font-size: 13px;">
|
||
<input type="checkbox" id="toggle-arrows" checked onchange="updateLayers()" style="accent-color: var(--primary); width: 16px; height: 16px;">
|
||
<span>اتجاهات الشوارع / Street Directions</span>
|
||
</label>
|
||
</div>
|
||
|
||
<!-- DB NAMES INDICATOR -->
|
||
<div id="db-status">
|
||
<div class="dot-gold"></div>
|
||
<span id="db-status-text">جاري تحميل الأسماء من قاعدة البيانات...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TOGGLE ADMIN -->
|
||
<button id="toggle-admin" onclick="toggleAdminPanel()" title="إدارة البيانات">⚙️</button>
|
||
|
||
<!-- CONTEXT MENU -->
|
||
<div id="context-menu">
|
||
<div class="menu-item" onclick="copyCoords()">
|
||
<span class="icon">📋</span> <span>نسخ الإحداثيات</span>
|
||
</div>
|
||
<div class="menu-item" id="menu-delete-btn" style="color:#c0392b" onclick="deleteFromContext()">
|
||
<span class="icon">🗑️</span> <span>حذف هذا الموقع</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ADMIN PANEL -->
|
||
<div id="admin-panel">
|
||
<div class="admin-title">
|
||
<span>🛡️ إدارة البيانات</span>
|
||
<span class="admin-close" onclick="toggleAdminPanel()">×</span>
|
||
</div>
|
||
<div class="delete-group">
|
||
<label>حذف موقع بواسطة الاسم</label>
|
||
<input type="text" id="admin-delete-name" placeholder="أدخل اسم الموقع..." />
|
||
<label>الدولة</label>
|
||
<select id="admin-delete-country">
|
||
<option value="syria">سوريا</option>
|
||
<option value="jordan">الأردن</option>
|
||
<option value="egypt">مصر</option>
|
||
</select>
|
||
<button class="btn-delete" onclick="handleAdminDelete()">حذف الموقع نهائياً</button>
|
||
</div>
|
||
<div id="admin-info" style="font-size:11px; margin-top:10px; opacity:0.6; text-align:center"></div>
|
||
</div>
|
||
|
||
<script>
|
||
/* ───────────────────────────────────────────
|
||
CONFIG
|
||
─────────────────────────────────────────── */
|
||
const API_BASE = window.location.origin; // Dynamically use the current host
|
||
const API_KEY = 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX';
|
||
const HEADERS = { 'x-api-key': API_KEY };
|
||
|
||
const ROUTES = {
|
||
LEVANT: {
|
||
from: [35.9106, 31.9539], // Amman
|
||
to: [36.2765, 33.5138], // Damascus
|
||
center: [36.09, 32.73],
|
||
zoom: 8,
|
||
color: '#c0a048'
|
||
},
|
||
EGYPT: {
|
||
from: [31.2427, 30.0931], // Shoubra
|
||
to: [31.2201, 30.0619], // Zamalek
|
||
center: [31.23, 30.07],
|
||
zoom: 13,
|
||
color: '#2563eb'
|
||
}
|
||
};
|
||
|
||
/* ───────────────────────────────────────────
|
||
MAP INIT
|
||
─────────────────────────────────────────── */
|
||
maplibregl.setRTLTextPlugin(
|
||
'https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.2.3/mapbox-gl-rtl-text.js',
|
||
null, true
|
||
);
|
||
|
||
const map = new maplibregl.Map({
|
||
container: 'map',
|
||
center: ROUTES.LEVANT.center,
|
||
zoom: ROUTES.LEVANT.zoom,
|
||
style: './style.json', // ← style.json fixed (no duplicate IDs)
|
||
pitch: 0,
|
||
bearing: 0,
|
||
attributionControl: false
|
||
});
|
||
|
||
map.addControl(new maplibregl.NavigationControl(), 'bottom-right');
|
||
|
||
/* ───────────────────────────────────────────
|
||
MAP LOAD — count DB names shown on map
|
||
─────────────────────────────────────────── */
|
||
map.on('load', () => {
|
||
console.log('[Intaleq] Map loaded — style v3.1.0');
|
||
loadDBNamesStatus();
|
||
});
|
||
|
||
/*
|
||
* Fetch the same GeoJSON the style uses for intaleq_dynamic_pois
|
||
* and report how many names were loaded on the map.
|
||
*/
|
||
async function loadDBNamesStatus() {
|
||
const statusEl = document.getElementById('db-status-text');
|
||
try {
|
||
const res = await fetch(`${API_BASE}/api/geocoding/geojson`, { headers: HEADERS });
|
||
const data = await res.json();
|
||
const count = data?.features?.length ?? 0;
|
||
|
||
statusEl.textContent = count > 0
|
||
? `✅ ${count} اسم مُحمَّل من قاعدة البيانات على الخريطة`
|
||
: '⚠️ لا توجد أسماء في قاعدة البيانات حتى الآن';
|
||
|
||
// Refresh the source in case MapLibre cached an empty response
|
||
if (count > 0 && map.getSource('intaleq_dynamic_pois')) {
|
||
map.getSource('intaleq_dynamic_pois').setData(data);
|
||
}
|
||
} catch (err) {
|
||
console.warn('[Intaleq] DB names fetch failed:', err);
|
||
statusEl.textContent = '⚠️ تعذّر الاتصال بقاعدة البيانات';
|
||
}
|
||
}
|
||
|
||
/* ───────────────────────────────────────────
|
||
POLYLINE DECODER
|
||
─────────────────────────────────────────── */
|
||
function decodePoly(str) {
|
||
let i = 0, lat = 0, lng = 0, out = [];
|
||
while (i < str.length) {
|
||
let byte, shift = 0, result = 0;
|
||
do { byte = str.charCodeAt(i++) - 63; result |= (byte & 0x1f) << shift; shift += 5; } while (byte >= 0x20);
|
||
lat += (result & 1) ? ~(result >> 1) : (result >> 1);
|
||
shift = 0; result = 0;
|
||
do { byte = str.charCodeAt(i++) - 63; result |= (byte & 0x1f) << shift; shift += 5; } while (byte >= 0x20);
|
||
lng += (result & 1) ? ~(result >> 1) : (result >> 1);
|
||
out.push([lng / 1e5, lat / 1e5]);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/* ───────────────────────────────────────────
|
||
ROUTING
|
||
─────────────────────────────────────────── */
|
||
async function handleRoute(region) {
|
||
const cfg = ROUTES[region];
|
||
const loading = document.getElementById('loading');
|
||
const status = document.getElementById('status');
|
||
|
||
loading.style.display = 'flex';
|
||
status.style.display = 'none';
|
||
|
||
try {
|
||
const url = `${API_BASE}/api/maps/route`
|
||
+ `?fromLat=${cfg.from[1]}&fromLng=${cfg.from[0]}`
|
||
+ `&toLat=${cfg.to[1]}&toLng=${cfg.to[0]}`;
|
||
|
||
const res = await fetch(url, { headers: HEADERS });
|
||
const data = await res.json();
|
||
|
||
if (data.statusCode >= 400) throw new Error(data.message || 'Routing error');
|
||
|
||
const coords = typeof data.points === 'string'
|
||
? decodePoly(data.points)
|
||
: data.points;
|
||
|
||
if (!coords?.length) throw new Error('No route points returned');
|
||
|
||
// Add or update MAIN route layer
|
||
const geojson = { type: 'Feature', geometry: { type: 'LineString', coordinates: coords } };
|
||
|
||
if (map.getSource('route')) {
|
||
map.getSource('route').setData(geojson);
|
||
map.setPaintProperty('route-line', 'line-color', cfg.color);
|
||
} else {
|
||
map.addSource('route', { type: 'geojson', data: geojson });
|
||
map.addLayer({
|
||
id: 'route-line', type: 'line', source: 'route',
|
||
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
||
paint: { 'line-color': cfg.color, 'line-width': 7, 'line-opacity': 0.88 }
|
||
});
|
||
}
|
||
|
||
// Add or update ALTERNATIVE route layer
|
||
if (data.alternatives && data.alternatives.length > 0) {
|
||
const altCoords = typeof data.alternatives[0].points === 'string'
|
||
? decodePoly(data.alternatives[0].points)
|
||
: data.alternatives[0].points;
|
||
|
||
const altGeojson = { type: 'Feature', geometry: { type: 'LineString', coordinates: altCoords } };
|
||
|
||
if (map.getSource('route-alt')) {
|
||
map.getSource('route-alt').setData(altGeojson);
|
||
} else {
|
||
map.addSource('route-alt', { type: 'geojson', data: altGeojson });
|
||
map.addLayer({
|
||
id: 'route-line-alt', type: 'line', source: 'route-alt',
|
||
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
||
paint: { 'line-color': '#718096', 'line-width': 5, 'line-opacity': 0.65, 'line-dasharray': [2, 2] }
|
||
}, 'route-line'); // Draw below main route
|
||
}
|
||
} else {
|
||
// Clear alternative route if none exists
|
||
if (map.getSource('route-alt')) {
|
||
map.getSource('route-alt').setData({ type: 'FeatureCollection', features: [] });
|
||
}
|
||
}
|
||
|
||
// Fit map to route
|
||
const bounds = coords.reduce(
|
||
(b, c) => b.extend(c),
|
||
new maplibregl.LngLatBounds(coords[0], coords[0])
|
||
);
|
||
map.fitBounds(bounds, { padding: 110, duration: 1800 });
|
||
|
||
// Show summary
|
||
setTimeout(() => {
|
||
status.style.display = 'block';
|
||
status.innerHTML = `
|
||
<div style="font-weight:700;color:var(--primary-dark);margin-bottom:8px">🏁 ملخص الرحلة</div>
|
||
<div class="status-row"><span>المسافة:</span><span>${(data.distance / 1000).toFixed(1)} كم</span></div>
|
||
<div class="status-row"><span>الوقت المتوقع:</span><span>${Math.round(data.duration / 60)} دقيقة</span></div>
|
||
`;
|
||
}, 600);
|
||
|
||
} catch (err) {
|
||
console.error('[Intaleq] Route error:', err);
|
||
status.style.display = 'block';
|
||
status.innerHTML = `<span style="color:#c0392b">⚠️ تعذّر تحميل المسار: ${err.message}</span>`;
|
||
} finally {
|
||
loading.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
/* ───────────────────────────────────────────
|
||
SEARCH
|
||
─────────────────────────────────────────── */
|
||
let searchMarkers = [];
|
||
|
||
async function performSearch() {
|
||
const q = document.getElementById('search-input').value.trim();
|
||
const resultsEl = document.getElementById('search-results');
|
||
if (!q) return;
|
||
|
||
const center = map.getCenter();
|
||
const url = `${API_BASE}/api/geocoding/search`
|
||
+ `?q=${encodeURIComponent(q)}`
|
||
+ `&lat=${center.lat}&lng=${center.lng}&radius=20000`;
|
||
|
||
resultsEl.style.display = 'block';
|
||
resultsEl.innerHTML = '<div class="result-item"><span class="meta">جاري البحث...</span></div>';
|
||
|
||
// Clear old markers
|
||
searchMarkers.forEach(m => m.remove());
|
||
searchMarkers = [];
|
||
|
||
try {
|
||
const res = await fetch(url, { headers: HEADERS });
|
||
const data = await res.json();
|
||
|
||
if (!data.results?.length) {
|
||
resultsEl.innerHTML = '<div class="result-item"><span class="meta">لا توجد نتائج.</span></div>';
|
||
return;
|
||
}
|
||
|
||
resultsEl.innerHTML = '';
|
||
data.results.forEach(place => {
|
||
const lng = parseFloat(place.longitude);
|
||
const lat = parseFloat(place.latitude);
|
||
const name = place.name_ar || place.name;
|
||
const dist = place.distance ? `${(place.distance / 1000).toFixed(2)} كم` : '';
|
||
|
||
// Result row
|
||
const item = document.createElement('div');
|
||
item.className = 'result-item';
|
||
item.innerHTML = `
|
||
<div class="name">${name}</div>
|
||
<div class="meta">${place.category || ''} ${dist ? '• ' + dist : ''}</div>
|
||
`;
|
||
item.onclick = () => map.flyTo({ center: [lng, lat], zoom: 16 });
|
||
resultsEl.appendChild(item);
|
||
|
||
// Map marker — gold for DB entries, blue for OSM
|
||
const isDB = place.source === 'user_submitted' || place.source === 'intaleq_db';
|
||
const color = isDB ? '#c0a048' : '#2563eb';
|
||
|
||
let popupHTML = `<strong>${name}</strong><br><span style="font-size:12px;color:#888">${place.category || ''}</span>`;
|
||
if (isDB) {
|
||
popupHTML += `<hr style="margin:8px 0; border:0; border-top:1px solid #eee">
|
||
<button onclick="directDelete(${place.id})" style="background:#c0392b; color:#fff; border:0; padding:4px 8px; border-radius:4px; font-size:10px; cursor:pointer; width:100%">حذف القيد</button>`;
|
||
}
|
||
|
||
const marker = new maplibregl.Marker({ color })
|
||
.setLngLat([lng, lat])
|
||
.setPopup(new maplibregl.Popup({ offset: 25 })
|
||
.setHTML(popupHTML))
|
||
.addTo(map);
|
||
|
||
searchMarkers.push(marker);
|
||
});
|
||
|
||
} catch (err) {
|
||
console.error('[Intaleq] Search error:', err);
|
||
resultsEl.innerHTML = '<div class="result-item" style="color:#c0392b">⚠️ فشل الاتصال بخدمة البحث.</div>';
|
||
}
|
||
}
|
||
|
||
// Enter key triggers search
|
||
document.getElementById('search-input').addEventListener('keydown', e => {
|
||
if (e.key === 'Enter') performSearch();
|
||
});
|
||
|
||
/* ───────────────────────────────────────────
|
||
INTERACTIVE MANAGEMENT
|
||
─────────────────────────────────────────── */
|
||
let lastRightClick = null;
|
||
|
||
map.on('contextmenu', (e) => {
|
||
lastRightClick = e.lngLat;
|
||
const menu = document.getElementById('context-menu');
|
||
menu.style.display = 'block';
|
||
menu.style.left = e.point.x + 'px';
|
||
menu.style.top = e.point.y + 'px';
|
||
});
|
||
|
||
document.addEventListener('click', () => {
|
||
document.getElementById('context-menu').style.display = 'none';
|
||
});
|
||
|
||
function copyCoords() {
|
||
if (!lastRightClick) return;
|
||
const txt = `${lastRightClick.lat.toFixed(7)}, ${lastRightClick.lng.toFixed(7)}`;
|
||
navigator.clipboard.writeText(txt);
|
||
alert('تم نسخ الإحداثيات: ' + txt);
|
||
}
|
||
|
||
async function deleteFromContext() {
|
||
if (!lastRightClick) return;
|
||
const country = prompt('يرجى كتابة الدولة للتأكيد (syria, jordan, egypt):', 'syria');
|
||
if (!country) return;
|
||
|
||
const name = prompt('يرجى كتابة اسم الموقع لحذفه نهائياً:');
|
||
if (!name) return;
|
||
|
||
await apiDelete(name, country);
|
||
}
|
||
|
||
function toggleAdminPanel() {
|
||
const p = document.getElementById('admin-panel');
|
||
p.style.display = p.style.display === 'block' ? 'none' : 'block';
|
||
}
|
||
|
||
async function handleAdminDelete() {
|
||
const name = document.getElementById('admin-delete-name').value.trim();
|
||
const country = document.getElementById('admin-delete-country').value;
|
||
if (!name) return alert('يرجى إدخال الاسم');
|
||
if (!confirm(`هل أنت متأكد من حذف 모든 المواقع المسمى "${name}" في ${country}؟`)) return;
|
||
|
||
await apiDelete(name, country);
|
||
}
|
||
|
||
async function directDelete(id) {
|
||
const country = prompt('يرجى تحديد الدولة (syria, jordan, egypt):', 'syria');
|
||
if (!country) return;
|
||
if (!confirm('هل أنت متأكد من حذف هذا القيد؟')) return;
|
||
|
||
try {
|
||
const res = await fetch(`${API_BASE}/api/geocoding/places?id=${id}&country=${country}`, {
|
||
method: 'DELETE',
|
||
headers: HEADERS
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
alert('تم الحذف بنجاح');
|
||
location.reload();
|
||
}
|
||
} catch (err) {
|
||
alert('فشل الحذف');
|
||
}
|
||
}
|
||
|
||
async function apiDelete(name, country) {
|
||
const info = document.getElementById('admin-info');
|
||
info.textContent = 'جاري الحذف...';
|
||
try {
|
||
const res = await fetch(`${API_BASE}/api/geocoding/places?name=${encodeURIComponent(name)}&country=${country}`, {
|
||
method: 'DELETE',
|
||
headers: HEADERS
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
info.textContent = `✅ تم حذف ${data.affected} قيد بنجاح.`;
|
||
setTimeout(() => location.reload(), 2000);
|
||
} else {
|
||
info.textContent = '❌ فشل الحذف.';
|
||
}
|
||
} catch (err) {
|
||
info.textContent = '❌ خطأ في الاتصال بالسيرفر.';
|
||
}
|
||
}
|
||
|
||
/* ───────────────────────────────────────────
|
||
LAYER CONTROLS
|
||
─────────────────────────────────────────── */
|
||
function updateLayers() {
|
||
const show3D = document.getElementById('toggle-3d').checked;
|
||
const showPOIs = document.getElementById('toggle-pois').checked;
|
||
const showArrows = document.getElementById('toggle-arrows').checked;
|
||
|
||
// Toggle 3D Overture buildings
|
||
if (map.getLayer('building-3d')) {
|
||
map.setLayoutProperty('building-3d', 'visibility', show3D ? 'visible' : 'none');
|
||
}
|
||
// Show flat footprints when 3D is off
|
||
if (map.getLayer('overture-building-footprint')) {
|
||
map.setLayoutProperty('overture-building-footprint', 'visibility', show3D ? 'none' : 'visible');
|
||
}
|
||
|
||
if (map.getLayer('intaleq-db-label')) {
|
||
map.setLayoutProperty('intaleq-db-label', 'visibility', showPOIs ? 'visible' : 'none');
|
||
}
|
||
|
||
// Also toggle the dynamic POIs icons layer if exists
|
||
if (map.getLayer('intaleq_pois')) {
|
||
map.setLayoutProperty('intaleq_pois', 'visibility', showPOIs ? 'visible' : 'none');
|
||
}
|
||
|
||
// Toggle places label layers + Overture name layers
|
||
['places-syria-labels', 'places-jordan-labels', 'places-egypt-labels', 'overture-building-names', 'overture-street-names'].forEach(id => {
|
||
if (map.getLayer(id)) {
|
||
map.setLayoutProperty(id, 'visibility', showPOIs ? 'visible' : 'none');
|
||
}
|
||
});
|
||
|
||
// Toggle street direction arrows
|
||
if (map.getLayer('road-direction-arrows')) {
|
||
map.setLayoutProperty('road-direction-arrows', 'visibility', showArrows ? 'visible' : 'none');
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|