feat: add Jordan 3D topographic platform, Siro Map cinematic orbit studio, sun-shadow simulation, and sovereign intelligence security gating
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
#!/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!")
|
||||
Reference in New Issue
Block a user