1let projectsData = null; 2let globe = null; 3 4 5const isMobile = /Mobi|Android/i.test(navigator.userAgent); 6 7// ---------- HELPERS ---------- 8// Global waitForElement helper (used by all scripts) 9window.waitForElement = function(selector, callback, timeout = 5000) { 10 const check = () => { 11 const el = document.querySelector(selector); 12 if (el) { 13 callback(el); 14 return true; 15 } 16 return false; 17 }; 18 if (check()) return; 19 const interval = setInterval(() => { if (check()) clearInterval(interval); }, 100); 20 setTimeout(() => clearInterval(interval), timeout); 21}; 22 23const fetchProjects = () => { 24 projectsData = GLOBE_DATA; 25 return Promise.resolve(projectsData); 26} 27 28const loadScript = (src) => 29 new Promise((resolve, reject) => { 30 const s = document.createElement("script"); 31 s.src = src; 32 s.onload = resolve; 33 s.onerror = reject; 34 document.body.appendChild(s); 35 }); 36 37const waitForElements = (selector, callback, timeout = 3000) => { 38 const interval = setInterval(() => { 39 if (document.querySelectorAll(selector).length) { 40 clearInterval(interval); 41 callback(); 42 } 43 }, 100); 44 setTimeout(() => clearInterval(interval), timeout); 45}; 46 47// ---------- INIT ---------- 48const hasGlobeSection = !!document.querySelector(".section__globe"); 49// const projectsPromise = hasGlobeSection ? fetchProjects() : Promise.resolve(); 50 51waitForElement(".section__globe", (target) => { 52 let initialized = false; 53 54 const io = new IntersectionObserver(async (entries, obs) => { 55 const entry = entries[0]; 56 if (!entry.isIntersecting || initialized) return; 57 initialized = true; 58 obs.disconnect(); 59 60 try { 61 await loadScript("https://unpkg.com/globe.gl"); 62 await fetchProjects(); 63 /* 64 await Promise.all([ 65 loadScript("https://unpkg.com/globe.gl"), 66 projectsPromise, 67 ]); 68 */ 69 70 if (!projectsData?.items?.length) { 71 console.error("[Globe] No data found â aborting init"); 72 return; 73 } 74 75 initGlobe(); 76 setTimeout(() => ScrollTrigger?.refresh(true), 1500); 77 } catch (e) { 78 console.error("[Globe] Initialization failed:", e); 79 } 80 }, { rootMargin: "200px" }); 81 82 io.observe(target); 83}); 84 85// ---------- MAIN GLOBE FUNCTION ---------- 86function initGlobe() { 87 waitForElements(".section__globe", () => { 88 console.log("[Globe] All collection items loaded"); 89 90 let shouldRotate = true; 91 let expandedTooltip = null; 92 let expandTimeout = null; 93 let isMobileOutsideCloseBound = false; 94 95 fetch("https://unpkg.com/globe.gl@latest/example/datasets/ne_110m_admin_0_countries.geojson") 96 .then((res) => res.json()) 97 .then((countries) => { 98 const globeEl = document.getElementById("globeVisual"); 99 if (!globeEl) return console.warn("[Globe] #globeVisual missing."); 100 101 globe = Globe()(globeEl) 102 .backgroundColor("rgba(0,0,0,0)") 103 .atmosphereColor("#d4d4d4") 104 .atmosphereAltitude(0.09) 105 .globeImageUrl("https://cdn.prod.website-files.com/6808dadd6ac838b74859424e/693ace8d24a67299cb314664_globe-texture_dark_gray.jpg") 106 .pointOfView({ lat: 42.7, lng: 23.3, altitude: 2 }) 107 .hexPolygonsData(countries.features) 108 .hexPolygonResolution(3) 109 .hexPolygonMargin(0.3) 110 .hexPolygonUseDots(true) 111 .hexPolygonColor((d) => { 112 const countryName = d.properties?.NAME || d.properties?.NAME_EN || ""; 113 const nameLower = countryName.toLowerCase(); 114 115 // gray color for China and Russia 116 if (nameLower.includes("china") || nameLower.includes("киÑай") || 117 nameLower.includes("russia") || nameLower.includes("ÑоÑÑиÑ") || 118 nameLower.includes("russian federation")) { 119 return `rgb(73, 73, 73)`; // gray color 120 } 121 122 // orange color for other countries 123 return `rgb(245, 161, 69)`; 124 }); 125 126 const rawLocations = projectsData.items 127 .map((item) => { 128 return { 129 name: item.name ?? "", 130 lat: Number(item["lat"]), 131 lng: Number(item["lng"]), 132 logo: item.logo ?? null, 133 location: item["location"] ?? "", 134 public: item["public"] ?? true, 135 }; 136 }) 137 .filter((x) => Number.isFinite(x.lat) && Number.isFinite(x.lng) && x.public); 138 139 const locationMap = new Map(); 140 rawLocations.forEach((loc) => {
141 const key = `${loc.lat},${loc.lng}`; 142 if (!locationMap.has(key)) locationMap.set(key, []); 143 locationMap.get(key).push(loc); 144 }); 145 146 const locationsNew = []; 147 locationMap.forEach((group) => { 148 locationsNew.push( 149 group.length === 1 ? group[0] : { ...group[0], places: group } 150 ); 151 }); 152 153 globe 154 .htmlElementsData(locationsNew) 155 .htmlLat((d) => d.lat) 156 .htmlLng((d) => d.lng) 157 .htmlAltitude(0.01) 158 .htmlElement((d) => createTooltipElement(d)); 159 160 const rotate = () => { 161 requestAnimationFrame(rotate); 162 globe.controls().autoRotate = shouldRotate; 163 globe.controls().autoRotateSpeed = 0.15; 164 globe.controls().update(); 165 }; 166 rotate(); 167 168 globe.controls().enableZoom = false; 169 170 // handle resize (throttled) 171 let resizeTimer; 172 window.addEventListener("resize", () => { 173 clearTimeout(resizeTimer); 174 resizeTimer = setTimeout(() => { 175 if (globeEl && globe) { 176 const globeHeight = isMobile ? 400 : Math.max(window.innerHeight - 180, 550); 177 globe.width(window.offsetWidth); 178 globe.height(globeHeight); 179 } 180 }, 200); 181 }); 182 183 184 // ---------- TOOLTIP ---------- 185 function createTooltipElement(d) { 186 const el = document.createElement("div"); 187 el.className = "tooltip_wrap"; 188 el.style.pointerEvents = "auto"; 189 190 const logoUrl = 191 d.logo || 192 "https://cdn.prod.website-files.com/6808dadd6ac838b74859424e/689cae86d110fcee4fa05dfa_webclip.png"; 193 194 el.innerHTML = d.places 195 ? ` 196 <div class="tooltip_content group"> 197 <div class="group-tooltip group-items-${d.places.length}"> 198 ${d.places 199 .map( 200 (p) => { 201 const logoUrl = p.logo || "https://cdn.prod.website-files.com/6808dadd6ac838b74859424e/689cae86d110fcee4fa05dfa_webclip.png"; 202 return ` 203 <div class="multiple-destination"> 204 <img src="${logoUrl}" width="24"> 205 <div class="tooltip__text-wrapper"> 206 <span class="tooltip__text p-tiny text-weight-medium">${p.name}</span> 207 <span class="p-micro text-secondary">${p.location}</span> 208 </div> 209 </div>`; 210 } 211 ) 212 .join("")} 213 </div> 214 </div>` 215 : ` 216 <div class="tooltip_content"> 217 <div class="single-destination"> 218 <img src="${logoUrl}" width="24"> 219 <div class="tooltip__text-wrapper"> 220 <span class="tooltip__text p-tiny text-weight-medium">${d.name}</span> 221 <span class="p-micro text-secondary">${d.location}</span> 222 </div> 223 </div> 224 </div>`; 225 226 const gWidth = isMobile ? window.innerWidth - 40 : window.innerWidth - 200; // globeEl ? globeEl.offsetWidth : window.innerWidth 227 const gHeight = isMobile ? 400 : Math.max(window.innerHeight - 250, 550); 228 globe.height(gHeight).width(gWidth); 229 230 const expand = () => { 231 const contents = el.querySelectorAll(".tooltip__text-wrapper"); 232 contents.forEach((content) => { 233 content.style.width = "auto"; 234 content.style.marginLeft = "0"; 235 const width = content.scrollWidth; 236 content.style.width = "0"; 237 content.offsetHeight; 238 requestAnimationFrame(() => { 239 content.style.width = `${7 + width}px`; 240 content.style.marginLeft = "8px"; 241 }); 242 }); 243 244 if (expandTimeout) clearTimeout(expandTimeout); 245 expandTimeout = setTimeout(() => el.classList.add("expand"), 500); 246 247 shouldRotate = false; 248 globe.controls().update(); 249 }; 250 251 const collapse = () => { 252 const contents = el.querySelectorAll(".tooltip__text-wrapper");
253 contents.forEach((content) => { 254 content.style.width = "0"; 255 content.style.marginLeft = "0"; 256 }); 257 if (expandTimeout) clearTimeout(expandTimeout); 258 el.classList.remove("expand"); 259 shouldRotate = true; 260 globe.controls().update(); 261 }; 262 263 // expose collapse so other tooltips can close this one reliably 264 el._collapseTooltip = collapse; 265 266 if (isMobile) { 267 el.addEventListener("touchstart", (e) => { 268 e.stopPropagation(); 269 270 // tap on the same tooltip closes it 271 if (expandedTooltip === el) { 272 collapse(); 273 expandedTooltip = null; 274 return; 275 } 276 277 if (expandedTooltip && expandedTooltip !== el) { 278 expandedTooltip._collapseTooltip?.(); 279 } 280 281 expand(); 282 expandedTooltip = el; 283 }); 284 285 if (!isMobileOutsideCloseBound) { 286 document.addEventListener("touchstart", (e) => { 287 if (expandedTooltip && !expandedTooltip.contains(e.target)) { 288 expandedTooltip._collapseTooltip?.(); 289 expandedTooltip = null; 290 } 291 }); 292 isMobileOutsideCloseBound = true; 293 } 294 } else { 295 el.addEventListener("mouseenter", () => { 296 if (expandedTooltip && expandedTooltip !== el) 297 collapse(expandedTooltip); 298 expand(); 299 expandedTooltip = el; 300 }); 301 302 el.addEventListener("mouseleave", () => { 303 collapse(el); 304 if (expandedTooltip === el) expandedTooltip = null; 305 }); 306 } 307 308 return el; 309 } 310 }); 311 }); 312}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.