329 lines
17 KiB
Python
329 lines
17 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Patch tourism.html files to dynamically load all 3,695 sovereign landmarks
|
|
from the NestJS API and static JSON fallback.
|
|
"""
|
|
|
|
import re
|
|
|
|
file_path = "apps/web/public/tourism.html"
|
|
with open(file_path, "r", encoding="utf-8") as f:
|
|
html = f.read()
|
|
|
|
# 1. Update Category Chips to include historic_shrine and religious_heritage
|
|
old_cat_bar = """ <div class="category-bar">
|
|
<button class="cat-chip active" data-cat="all">🌍 كافة المعالم</button>
|
|
<button class="cat-chip" data-cat="archaeological_site">🏛️ مواقع أثرية</button>
|
|
<button class="cat-chip" data-cat="historic_palace">🏰 قصور وقلاع</button>
|
|
<button class="cat-chip" data-cat="monument">🕌 صروح تاريخية</button>
|
|
<button class="cat-chip" data-cat="museum">🏺 متاحف سيادية</button>
|
|
<button class="cat-chip" data-cat="cultural_trail">🌿 مسارات بيئية وتراثية</button>
|
|
</div>"""
|
|
|
|
new_cat_bar = """ <div class="category-bar">
|
|
<button class="cat-chip active" data-cat="all">🌍 كافة المعالم</button>
|
|
<button class="cat-chip" data-cat="archaeological_site">🏛️ مواقع أثرية</button>
|
|
<button class="cat-chip" data-cat="historic_shrine">🕌 أضرحة ومقامات</button>
|
|
<button class="cat-chip" data-cat="religious_heritage">⛪ أديرة وتراث ديني</button>
|
|
<button class="cat-chip" data-cat="historic_palace">🏰 قصور وقلاع</button>
|
|
<button class="cat-chip" data-cat="monument">🗿 صروح تاريخية</button>
|
|
<button class="cat-chip" data-cat="museum">🏺 متاحف سيادية</button>
|
|
</div>"""
|
|
|
|
if old_cat_bar in html:
|
|
html = html.replace(old_cat_bar, new_cat_bar)
|
|
print("Updated category chips bar.")
|
|
|
|
# 2. Change const LANDMARKS = [ to let LANDMARKS = [
|
|
html = html.replace("const LANDMARKS = [", "let LANDMARKS = [")
|
|
|
|
# 3. Update country tab click listeners to flyTo country centers
|
|
old_country_listener = """ document.querySelectorAll('.country-btn').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
document.querySelectorAll('.country-btn').forEach(b => b.classList.remove('active'));
|
|
e.target.classList.add('active');
|
|
activeCountry = e.target.getAttribute('data-country');
|
|
renderMarkers();
|
|
});
|
|
});"""
|
|
|
|
new_country_listener = """ const COUNTRY_CENTERS = {
|
|
'all': { center: [38.0, 31.5], zoom: 5.5 },
|
|
'العراق': { center: [44.3661, 33.3152], zoom: 6.5 },
|
|
'الأردن': { center: [35.9284, 31.9454], zoom: 7.5 },
|
|
'سوريا': { center: [36.7136, 34.7325], zoom: 7.2 },
|
|
'مصر': { center: [31.2357, 29.5000], zoom: 6.5 }
|
|
};
|
|
|
|
document.querySelectorAll('.country-btn').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
document.querySelectorAll('.country-btn').forEach(b => b.classList.remove('active'));
|
|
e.target.classList.add('active');
|
|
activeCountry = e.target.getAttribute('data-country');
|
|
if (COUNTRY_CENTERS[activeCountry]) {
|
|
map.flyTo({
|
|
center: COUNTRY_CENTERS[activeCountry].center,
|
|
zoom: COUNTRY_CENTERS[activeCountry].zoom,
|
|
duration: 1800
|
|
});
|
|
}
|
|
renderMarkers();
|
|
});
|
|
});"""
|
|
|
|
if old_country_listener in html:
|
|
html = html.replace(old_country_listener, new_country_listener)
|
|
print("Updated country tab listener with flyTo.")
|
|
|
|
# 4. Enhance renderMarkers & renderSlider for smooth performance with thousands of items
|
|
old_render = """ // Render Markers for Landmarks
|
|
function renderMarkers() {
|
|
// Clear existing markers
|
|
currentMarkers.forEach(m => m.remove());
|
|
currentMarkers = [];
|
|
|
|
const filtered = LANDMARKS.filter(lm => {
|
|
const matchC = activeCountry === 'all' || lm.country === activeCountry;
|
|
const matchCat = activeCategory === 'all' || lm.category === activeCategory;
|
|
const matchS = !searchQuery || lm.name_ar.includes(searchQuery) || lm.name_en.toLowerCase().includes(searchQuery.toLowerCase());
|
|
return matchC && matchCat && matchS;
|
|
});
|
|
|
|
filtered.forEach(lm => {
|
|
// 1. Centroid Marker
|
|
const el = document.createElement('div');
|
|
el.className = 'marker-centroid';
|
|
el.innerHTML = '🏛️';
|
|
el.title = lm.name_ar;
|
|
el.addEventListener('click', () => selectLandmark(lm));
|
|
|
|
const m = new maplibregl.Marker({ element: el })
|
|
.setLngLat(lm.centroid)
|
|
.addTo(map);
|
|
currentMarkers.push(m);
|
|
|
|
// If this landmark is selected, also render its decoupled points (Gate, Parking, Ticket)
|
|
if (selectedLandmark && selectedLandmark.slug === lm.slug) {
|
|
// Gate
|
|
const gateEl = document.createElement('div');
|
|
gateEl.className = 'marker-gate';
|
|
gateEl.innerHTML = '🚪';
|
|
gateEl.title = `بوابة دخول: ${lm.name_ar}`;
|
|
gateEl.addEventListener('click', () => selectLandmark(lm));
|
|
currentMarkers.push(new maplibregl.Marker({ element: gateEl }).setLngLat(lm.gate).addTo(map));
|
|
|
|
// Parking
|
|
const parkEl = document.createElement('div');
|
|
parkEl.className = 'marker-parking';
|
|
parkEl.innerHTML = 'P';
|
|
parkEl.title = `مواقف سيارات: ${lm.name_ar}`;
|
|
currentMarkers.push(new maplibregl.Marker({ element: parkEl }).setLngLat(lm.parking).addTo(map));
|
|
|
|
// Ticket
|
|
const ticketEl = document.createElement('div');
|
|
ticketEl.className = 'marker-ticket';
|
|
ticketEl.innerHTML = '🎟️';
|
|
ticketEl.title = `مكتب تذاكر: ${lm.name_ar}`;
|
|
currentMarkers.push(new maplibregl.Marker({ element: ticketEl }).setLngLat(lm.ticket).addTo(map));
|
|
}
|
|
});
|
|
|
|
renderSlider(filtered);
|
|
}
|
|
|
|
// Render Bottom Slider
|
|
function renderSlider(list) {
|
|
const slider = document.getElementById('landmarkSlider');
|
|
slider.innerHTML = '';
|
|
list.forEach(lm => {
|
|
const card = document.createElement('div');
|
|
card.className = 'slider-card' + (selectedLandmark?.slug === lm.slug ? ' active' : '');
|
|
card.innerHTML = `
|
|
<img src="${lm.image}" class="slider-img" alt="${lm.name_ar}" onerror="this.style.display='none'">
|
|
<div class="slider-info">
|
|
<div class="slider-title">${lm.name_ar}</div>
|
|
<div class="slider-meta">📍 ${lm.governorate} • ${lm.era.split('/')[0]}</div>
|
|
</div>
|
|
`;
|
|
card.addEventListener('click', () => selectLandmark(lm));
|
|
slider.appendChild(card);
|
|
});
|
|
}"""
|
|
|
|
new_render = """ // Render Markers for Landmarks (Optimized for 3,600+ nodes)
|
|
function renderMarkers() {
|
|
currentMarkers.forEach(m => m.remove());
|
|
currentMarkers = [];
|
|
|
|
const filtered = LANDMARKS.filter(lm => {
|
|
const matchC = activeCountry === 'all' || lm.country === activeCountry;
|
|
const matchCat = activeCategory === 'all' || lm.category === activeCategory;
|
|
const matchS = !searchQuery || lm.name_ar.includes(searchQuery) || (lm.name_en && lm.name_en.toLowerCase().includes(searchQuery.toLowerCase()));
|
|
return matchC && matchCat && matchS;
|
|
});
|
|
|
|
// Update Header Subtitle Counter
|
|
const subtitleEl = document.querySelector('.header-title small');
|
|
if (subtitleEl) {
|
|
subtitleEl.textContent = `🏛️ خريطة التراث والآثار السيادية (${filtered.length.toLocaleString()} معلماً معتمداً)`;
|
|
}
|
|
|
|
// If activeCountry is all and no search, render top 200 landmarks to maintain silky 60fps
|
|
const toRender = (activeCountry === 'all' && !searchQuery) ? filtered.slice(0, 250) : filtered;
|
|
|
|
const categoryIcons = {
|
|
'archaeological_site': '🏛️',
|
|
'historic_shrine': '🕌',
|
|
'religious_heritage': '⛪',
|
|
'historic_palace': '🏰',
|
|
'monument': '🗿',
|
|
'museum': '🏺',
|
|
'cultural_trail': '🌿'
|
|
};
|
|
|
|
toRender.forEach(lm => {
|
|
const el = document.createElement('div');
|
|
el.className = 'marker-centroid';
|
|
el.innerHTML = categoryIcons[lm.category] || '🏛️';
|
|
el.title = `${lm.name_ar} (${lm.governorate})`;
|
|
el.addEventListener('click', () => selectLandmark(lm));
|
|
|
|
const m = new maplibregl.Marker({ element: el })
|
|
.setLngLat(lm.centroid)
|
|
.addTo(map);
|
|
currentMarkers.push(m);
|
|
|
|
if (selectedLandmark && selectedLandmark.slug === lm.slug) {
|
|
const gateEl = document.createElement('div');
|
|
gateEl.className = 'marker-gate';
|
|
gateEl.innerHTML = '🚪';
|
|
gateEl.title = `بوابة دخول: ${lm.name_ar}`;
|
|
gateEl.addEventListener('click', () => selectLandmark(lm));
|
|
currentMarkers.push(new maplibregl.Marker({ element: gateEl }).setLngLat(lm.gate).addTo(map));
|
|
|
|
const parkEl = document.createElement('div');
|
|
parkEl.className = 'marker-parking';
|
|
parkEl.innerHTML = 'P';
|
|
parkEl.title = `مواقف سيارات: ${lm.name_ar}`;
|
|
currentMarkers.push(new maplibregl.Marker({ element: parkEl }).setLngLat(lm.parking).addTo(map));
|
|
|
|
const ticketEl = document.createElement('div');
|
|
ticketEl.className = 'marker-ticket';
|
|
ticketEl.innerHTML = '🎟️';
|
|
ticketEl.title = `مكتب تذاكر: ${lm.name_ar}`;
|
|
currentMarkers.push(new maplibregl.Marker({ element: ticketEl }).setLngLat(lm.ticket).addTo(map));
|
|
}
|
|
});
|
|
|
|
renderSlider(filtered);
|
|
}
|
|
|
|
// Render Bottom Slider (Virtual slice for performance)
|
|
function renderSlider(list) {
|
|
const slider = document.getElementById('landmarkSlider');
|
|
slider.innerHTML = '';
|
|
const displaySlice = list.slice(0, 100);
|
|
displaySlice.forEach(lm => {
|
|
const card = document.createElement('div');
|
|
card.className = 'slider-card' + (selectedLandmark?.slug === lm.slug ? ' active' : '');
|
|
card.innerHTML = `
|
|
<img src="${lm.image}" class="slider-img" alt="${lm.name_ar}" onerror="this.src='https://upload.wikimedia.org/wikipedia/commons/7/78/Ishtar_Gate_Babylon_Iraq.jpg'">
|
|
<div class="slider-info">
|
|
<div class="slider-title">${lm.name_ar}</div>
|
|
<div class="slider-meta">📍 ${lm.governorate} • ${lm.era ? lm.era.split('/')[0] : ''}</div>
|
|
</div>
|
|
`;
|
|
card.addEventListener('click', () => selectLandmark(lm));
|
|
slider.appendChild(card);
|
|
});
|
|
}
|
|
|
|
// Dynamic Live Loader for All 3,695 Sovereign Landmarks
|
|
async function loadAllSovereignLandmarks() {
|
|
try {
|
|
const isServerEnv = window.location.hostname.includes('intaleqapp.com') || window.location.port === '3201' || window.location.port === '3204';
|
|
const apiUrl = isServerEnv ? '/api/v1/heritage/landmarks' : 'http://188.68.36.205:3200/api/v1/heritage/landmarks';
|
|
|
|
const res = await fetch(apiUrl, {
|
|
headers: { 'x-api-key': SOVEREIGN_MAP_API_KEY }
|
|
});
|
|
if (res.ok) {
|
|
const apiData = await res.json();
|
|
if (Array.isArray(apiData) && apiData.length > 50) {
|
|
LANDMARKS = apiData.map(item => ({
|
|
slug: item.slug,
|
|
name_ar: item.name_ar,
|
|
name_en: item.name_en || item.name_ar,
|
|
country: item.metadata?.country || (item.governorate?.includes('معان') || item.governorate?.includes('عمان') || item.governorate?.includes('الكرك') || item.governorate?.includes('إربد') || item.governorate?.includes('البلقاء') || item.governorate?.includes('عجلون') || item.governorate?.includes('جرش') || item.governorate?.includes('مادبا') || item.governorate?.includes('الطفيلة') || item.governorate?.includes('العقبة') || item.governorate?.includes('المفرق') || item.governorate?.includes('الزرقاء') ? 'الأردن' : (item.governorate?.includes('دمشق') || item.governorate?.includes('ريف دمشق') || item.governorate?.includes('حلب') || item.governorate?.includes('حمص') || item.governorate?.includes('حماة') || item.governorate?.includes('اللاذقية') || item.governorate?.includes('طرطوس') || item.governorate?.includes('إدلب') || item.governorate?.includes('درعا') || item.governorate?.includes('السويداء') || item.governorate?.includes('القنيطرة') || item.governorate?.includes('دير الزور') || item.governorate?.includes('الرقة') || item.governorate?.includes('الحسكة') ? 'سوريا' : (item.governorate?.includes('القاهرة') || item.governorate?.includes('الجيزة') || item.governorate?.includes('الإسكندرية') || item.governorate?.includes('الأقصر') || item.governorate?.includes('أسوان') || item.governorate?.includes('سوهاج') || item.governorate?.includes('قنا') || item.governorate?.includes('المنيا') || item.governorate?.includes('أسيوط') || item.governorate?.includes('الغربية') || item.governorate?.includes('الشرقية') || item.governorate?.includes('الدقهلية') || item.governorate?.includes('البحيرة') || item.governorate?.includes('كفر الشيخ') || item.governorate?.includes('المنوفية') || item.governorate?.includes('دمياط') || item.governorate?.includes('مطروح') || item.governorate?.includes('الوادي الجديد') || item.governorate?.includes('البحر الأحمر') || item.governorate?.includes('شمال سيناء') || item.governorate?.includes('جنوب سيناء') || item.governorate?.includes('بني سويف') || item.governorate?.includes('الفيوم') || item.governorate?.includes('القليوبية') || item.governorate?.includes('بورسعيد') || item.governorate?.includes('الإسماعيلية') || item.governorate?.includes('السويس') ? 'مصر' : 'العراق'))),
|
|
governorate: item.governorate,
|
|
category: item.category,
|
|
era: item.era || 'عصور تاريخية عريقة',
|
|
narrative: item.narrative_ar || item.narrative_en || '',
|
|
centroid: [item.coordinates.centroid.lng, item.coordinates.centroid.lat],
|
|
gate: item.coordinates.access_gate ? [item.coordinates.access_gate.lng, item.coordinates.access_gate.lat] : [item.coordinates.centroid.lng + 0.0006, item.coordinates.centroid.lat + 0.0004],
|
|
parking: item.coordinates.parking ? [item.coordinates.parking.lng, item.coordinates.parking.lat] : [item.coordinates.centroid.lng + 0.0012, item.coordinates.centroid.lat + 0.0008],
|
|
ticket: item.coordinates.ticket_office ? [item.coordinates.ticket_office.lng, item.coordinates.ticket_office.lat] : [item.coordinates.centroid.lng + 0.0005, item.coordinates.centroid.lat + 0.0003],
|
|
image: item.image_url || 'https://upload.wikimedia.org/wikipedia/commons/7/78/Ishtar_Gate_Babylon_Iraq.jpg',
|
|
unesco: item.metadata?.unesco_id ? `اليونسكو #${item.metadata.unesco_id}` : (item.metadata?.unesco || 'تراث وطني تاريخي مسجل'),
|
|
curator: item.metadata?.curator || 'الهيئة العامة للآثار والتراث',
|
|
hours: item.metadata?.opening_hours || '08:00 - 18:00 يومياً'
|
|
}));
|
|
console.log(`✅ Loaded ${LANDMARKS.length} landmarks live from PostGIS API!`);
|
|
renderMarkers();
|
|
return;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.warn('API live fetch fallback triggered...', err);
|
|
}
|
|
|
|
// Static JSON Fallback
|
|
try {
|
|
const jsonRes = await fetch('/data/landmarks_mena_full_4000.json');
|
|
if (jsonRes.ok) {
|
|
const jsonData = await jsonRes.json();
|
|
if (Array.isArray(jsonData) && jsonData.length > 0) {
|
|
LANDMARKS = jsonData;
|
|
console.log(`✅ Loaded ${LANDMARKS.length} landmarks from static JSON!`);
|
|
renderMarkers();
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load JSON', e);
|
|
}
|
|
}"""
|
|
|
|
if old_render in html:
|
|
html = html.replace(old_render, new_render)
|
|
print("Updated renderMarkers & renderSlider with live loader.")
|
|
|
|
# 5. Call loadAllSovereignLandmarks() right after map.on('load')
|
|
old_map_load = """ map.on('load', () => {
|
|
renderMarkers();
|
|
});"""
|
|
|
|
new_map_load = """ map.on('load', () => {
|
|
renderMarkers();
|
|
loadAllSovereignLandmarks();
|
|
});"""
|
|
|
|
if old_map_load in html:
|
|
html = html.replace(old_map_load, new_map_load)
|
|
print("Connected loadAllSovereignLandmarks to map load event.")
|
|
|
|
# Save back to all 4 destinations
|
|
targets = [
|
|
"apps/web/public/tourism.html",
|
|
"apps/web/public/tourism/index.html",
|
|
"apps/dashboard/tourism.html",
|
|
"apps/dashboard/tourism/index.html"
|
|
]
|
|
|
|
for t in targets:
|
|
with open(t, "w", encoding="utf-8") as f:
|
|
f.write(html)
|
|
print(f"Successfully patched {t}")
|
|
|
|
print("All tourism.html files updated successfully!")
|