import Globe from 'globe.gl'; import { orphanages } from './data.js'; const $ = sel => document.querySelector(sel); const globeEl = $('#globe'); const panelEl = $('#panel'); const resetBtn = $('#reset-view'); const hintEl = $('#hint'); const searchInput = $('#search-input'); const searchResults = $('#search-results'); const searchClear = $('#search-clear'); const tourBtn = $('#tour-btn'); const tourIcon = $('#tour-icon'); const tourLabel = $('#tour-label'); // ---- globe --------------------------------------------------------------- const world = new Globe(globeEl, { animateIn: true }) .backgroundColor('rgba(0,0,0,0)') .showAtmosphere(true) .atmosphereColor('#4ea8de') .atmosphereAltitude(0.18) .globeImageUrl('https://unpkg.com/three-globe/example/img/earth-night.jpg') .bumpImageUrl('https://unpkg.com/three-globe/example/img/earth-topology.png') .pointsData(orphanages) .pointLat(d => d.lat) .pointLng(d => d.lng) .pointAltitude(0.012) .pointRadius(0.55) .pointColor(d => '#d6a05c') .pointLabel(d => `
${d.city} · ${d.country}
${d.name}
~${d.children} children · founded ${d.founded}
`) .onPointClick(d => selectOrphanage(d)) .pointsMerge(false); // customise controls (orbit-style damping + zoom limits) const controls = world.controls(); controls.autoRotate = false; controls.enableDamping = true; controls.dampingFactor = 0.08; controls.rotateSpeed = 0.6; controls.zoomSpeed = 0.8; controls.minDistance = 180; controls.maxDistance = 700; // sizing function resize() { const w = globeEl.clientWidth; const h = globeEl.clientHeight; world.width(w).height(h); } resize(); window.addEventListener('resize', resize); // default view (a nice angle that shows all 10) const HOME_VIEW = { lat: 24, lng: 38, altitude: 2.4 }; world.pointOfView(HOME_VIEW, 0); resetBtn.addEventListener('click', () => { stopTour(); world.pointOfView(HOME_VIEW, 900); renderEmpty(); }); // ---- panel rendering ------------------------------------------------------ function renderEmpty() { panelEl.innerHTML = `
🌍

Pick a marker on the globe

You'll see the orphanage's name, the city and country, a short biography, photos, and roughly how many children live there.

`; fillCountryList(); if (hintEl) hintEl.textContent = 'Click any marker to open the home'; } function fillCountryList() { const ul = $('#country-list'); if (!ul) return; ul.innerHTML = orphanages .map((o, i) => `
  • ${i + 1}. ${o.name}${o.children}
  • ` ) .join(''); ul.querySelectorAll('li[data-id]').forEach(li => { li.addEventListener('click', () => { const o = orphanages.find(x => x.id === li.dataset.id); if (o) selectOrphanage(o); }); }); } function escapeHtml(s) { return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function selectOrphanage(o) { world.pointOfView({ lat: o.lat, lng: o.lng, altitude: 1.6 }, 900); // OSM embed: use the standard "export/embed.html" iframe endpoint. // bbox is roughly ±0.04 degrees around the marker. const d = 0.04; const bbox = `${o.lng - d}%2C${o.lat - d}%2C${o.lng + d}%2C${o.lat + d}`; const osmSrc = `https://www.openstreetmap.org/export/embed.html?bbox=${bbox}&layer=mapnik&marker=${o.lat}%2C${o.lng}`; const gmapsHref = `https://www.google.com/maps/search/?api=1&query=${o.lat}%2C${o.lng}`; const osmHref = `https://www.openstreetmap.org/?mlat=${o.lat}&mlon=${o.lng}#map=14/${o.lat}/${o.lng}`; panelEl.innerHTML = `
    ${escapeHtml(o.city)}

    ${escapeHtml(o.name)}

    ${escapeHtml(o.country)}
    ${o.children}
    Children
    City
    ${escapeHtml(o.city)}
    Country
    ${escapeHtml(o.country)}
    Founded
    ${o.founded}
    Community
    ${escapeHtml(o.religion)}

    ${escapeHtml(o.bio)}

    📍 Location · ${o.lat.toFixed(3)}, ${o.lng.toFixed(3)}
    ${o.photos .map( (url, i) => `
    ${escapeHtml(o.name)} photo ${i + 1} Photo ${i + 1} · click to zoom
    ` ) .join('')}
    `; panelEl.querySelectorAll('.photo').forEach(el => { el.addEventListener('click', () => openLightbox(el.dataset.url)); }); if (hintEl) hintEl.textContent = `Viewing · ${o.name}`; } function openLightbox(url) { const lb = document.createElement('div'); lb.className = 'lightbox open'; lb.innerHTML = `Enlarged photo`; lb.addEventListener('click', () => lb.remove()); document.body.appendChild(lb); } // ---- search --------------------------------------------------------------- function highlight(text, query) { if (!query) return escapeHtml(text); const idx = text.toLowerCase().indexOf(query.toLowerCase()); if (idx === -1) return escapeHtml(text); return ( escapeHtml(text.slice(0, idx)) + '' + escapeHtml(text.slice(idx, idx + query.length)) + '' + escapeHtml(text.slice(idx + query.length)) ); } function runSearch() { const q = searchInput.value.trim(); if (!q) { searchResults.hidden = true; searchClear.hidden = true; return; } searchClear.hidden = false; const ql = q.toLowerCase(); const matches = orphanages.filter(o => o.name.toLowerCase().includes(ql) || o.city.toLowerCase().includes(ql) || o.country.toLowerCase().includes(ql) ); if (matches.length === 0) { searchResults.innerHTML = `
  • No match for "${escapeHtml(q)}"
  • `; } else { searchResults.innerHTML = matches .slice(0, 6) .map(o => `
  • ${highlight(o.name, q)}
    ${highlight(o.city, q)} · ${highlight(o.country, q)} · ${o.children} children
  • `) .join(''); searchResults.querySelectorAll('li[data-id]').forEach(li => { li.addEventListener('click', () => { const o = orphanages.find(x => x.id === li.dataset.id); if (o) { stopTour(); selectOrphanage(o); searchResults.hidden = true; } }); }); } searchResults.hidden = false; } searchInput.addEventListener('input', runSearch); searchInput.addEventListener('focus', () => { if (searchInput.value.trim()) searchResults.hidden = false; }); searchInput.addEventListener('keydown', e => { if (e.key === 'Escape') { searchInput.value = ''; searchResults.hidden = true; searchClear.hidden = true; searchInput.blur(); } }); searchClear.addEventListener('click', () => { searchInput.value = ''; searchResults.hidden = true; searchClear.hidden = true; searchInput.focus(); }); document.addEventListener('click', e => { if (!e.target.closest('.search-bar')) searchResults.hidden = true; }); // ---- auto-tour ------------------------------------------------------------ let tourTimer = null; let tourIndex = 0; const TOUR_INTERVAL = 4200; // ms per stop const TOUR_TRANSITION = 1400; // ms for camera fly function startTour() { if (tourTimer) return; tourIndex = 0; tourBtn.classList.add('is-playing'); tourIcon.textContent = '■'; tourLabel.textContent = 'Stop Tour'; if (hintEl) hintEl.textContent = 'Tour playing · click Stop to exit'; stepTour(); tourTimer = setInterval(stepTour, TOUR_INTERVAL); } function stepTour() { const o = orphanages[tourIndex % orphanages.length]; tourIndex++; selectOrphanage(o); } function stopTour() { if (!tourTimer) return; clearInterval(tourTimer); tourTimer = null; tourBtn.classList.remove('is-playing'); tourIcon.textContent = '▶'; tourLabel.textContent = 'Play Tour'; if (hintEl) hintEl.textContent = 'Click any marker to open the home'; } tourBtn.addEventListener('click', () => { if (tourTimer) stopTour(); else startTour(); }); // initial empty panel renderEmpty(); // dev-only debug helper, harmless in prod window.__select = id => { const o = orphanages.find(x => x.id === id); if (o) selectOrphanage(o); }; window.__tour = (on = true) => (on ? startTour() : stopTour());