#!/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 = """
🌍 كافة المعالم
🏛️ مواقع أثرية
🏰 قصور وقلاع
🕌 صروح تاريخية
🏺 متاحف سيادية
🌿 مسارات بيئية وتراثية
"""
new_cat_bar = """
🌍 كافة المعالم
🏛️ مواقع أثرية
🕌 أضرحة ومقامات
⛪ أديرة وتراث ديني
🏰 قصور وقلاع
🗿 صروح تاريخية
🏺 متاحف سيادية
"""
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 = `
${lm.name_ar}
📍 ${lm.governorate} • ${lm.era.split('/')[0]}
`;
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 = `
${lm.name_ar}
📍 ${lm.governorate} • ${lm.era ? lm.era.split('/')[0] : ''}
`;
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!")