PageSourceSearch

https://ast-science.com/wp-content/themes/spacemobile-2025/assets/js/globe.js?ver=1783660017

js ast-science.com collected 2026-10-02 04:20:56 UTC 10,608 bytes, 312 lines download raw bytes

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.