/* assets/globe.js */ import { renderMarkdownHtml } from "/assets/markdown.js?v=1"; const MAPLIBRE_VERSION = "5.24.0"; const MAPLIBRE_BASE = `https://unpkg.com/maplibre-gl@${MAPLIBRE_VERSION}/dist`; function ensureMapLibre() { if (typeof maplibregl !== "undefined") { return Promise.resolve(); } if (window.__maplibreLoadPromise) { return window.__maplibreLoadPromise; } window.__maplibreLoadPromise = new Promise((resolve, reject) => { if (!document.querySelector('link[data-globe-maplibre-css="1"]')) { const link = document.createElement("link"); link.rel = "stylesheet"; link.href = `${MAPLIBRE_BASE}/maplibre-gl.css`; link.dataset.globeMaplibreCss = "1"; document.head.appendChild(link); } const script = document.createElement("script"); script.src = `${MAPLIBRE_BASE}/maplibre-gl.js`; script.onload = () => resolve(); script.onerror = () => reject(new Error("Failed to load MapLibre GL")); document.head.appendChild(script); }); return window.__maplibreLoadPromise; } const GLOBE_DEFAULTS = { style: "https://tiles.openfreemap.org/styles/liberty", center: [0, 20], zoom: 1.5, minZoom: 0, maxZoom: 22, bearing: 0, pitch: 0, minPitch: 0, maxPitch: 85, projection: "globe", attributionControl: true, scrollZoom: true, dragRotate: true, dragPan: true, keyboard: true, doubleClickZoom: true, touchZoomRotate: true, interactive: true, showGeolocateControl: false, useCurrentLocation: false, geolocateZoom: 10, showUserLocation: true, }; function parseBool(value, fallback) { if (value === undefined) return fallback; return value === "true" || value === "1"; } function parseNumber(value, fallback) { if (value === undefined || value === "") return fallback; const n = Number(value); return Number.isFinite(n) ? n : fallback; } function parseCenter(value, fallback) { if (!value) return fallback; try { const parsed = JSON.parse(value); if (Array.isArray(parsed) && parsed.length === 2) return parsed; } catch { const parts = value.split(",").map((s) => Number(s.trim())); if (parts.length === 2 && parts.every(Number.isFinite)) return parts; } return fallback; } function readOptionsFromElement(el) { const d = el.dataset; return { style: d.style, center: parseCenter(d.center, undefined), zoom: parseNumber(d.zoom, undefined), minZoom: parseNumber(d.minZoom, undefined), maxZoom: parseNumber(d.maxZoom, undefined), bearing: parseNumber(d.bearing, undefined), pitch: parseNumber(d.pitch, undefined), minPitch: parseNumber(d.minPitch, undefined), maxPitch: parseNumber(d.maxPitch, undefined), projection: d.projection, attributionControl: parseBool(d.attributionControl, undefined), scrollZoom: parseBool(d.scrollZoom, undefined), dragRotate: parseBool(d.dragRotate, undefined), dragPan: parseBool(d.dragPan, undefined), keyboard: parseBool(d.keyboard, undefined), doubleClickZoom: parseBool(d.doubleClickZoom, undefined), touchZoomRotate: parseBool(d.touchZoomRotate, undefined), interactive: parseBool(d.interactive, undefined), showGeolocateControl: parseBool(d.showGeolocateControl, undefined), useCurrentLocation: parseBool(d.useCurrentLocation, undefined), geolocateZoom: parseNumber(d.geolocateZoom, undefined), showUserLocation: parseBool(d.showUserLocation, undefined), }; } function mergeGlobeOptions(...sources) { const merged = { ...GLOBE_DEFAULTS }; for (const source of sources) { if (!source) continue; for (const [key, value] of Object.entries(source)) { if (value !== undefined) merged[key] = value; } } return merged; } const HIGHLIGHT_SOURCE = "borderless-highlights"; const HIGHLIGHT_LAYER = "borderless-highlight-circles"; const DETAIL_SECTIONS = [ ["why_recommended", "Why This Country Is Recommended"], ["feasible_methods", "Feasible Migration Methods"], ["procedure", "Step-by-Step Procedure"], ["requirements", "Requirements And Documents"], ["duration_costs_risks", "Duration, Costs, And Risks"], ["official_sources", "Official Sources"], ]; window.BorderlessGlobe = { map: null, markers: [], markerRecords: [], lastVersion: -1, lastDetailsVersion: -1, pendingState: null, rootElement: null, countryDetails: {}, detailModalBackdrop: null, detailModalTitle: null, detailModalPathway: null, detailModalBody: null, shell() { return this.rootElement?.querySelector(".globe-shell") ?? null; }, updateShellState(hasMarkers) { const shell = this.shell(); if (!shell) return; shell.classList.toggle("has-markers", Boolean(hasMarkers)); }, clearMarkers() { for (const marker of this.markers) { marker.remove(); } this.markers = []; this.markerRecords = []; }, clearHighlights() { if (!this.map) return; if (this.map.getLayer(HIGHLIGHT_LAYER)) { this.map.removeLayer(HIGHLIGHT_LAYER); } if (this.map.getSource(HIGHLIGHT_SOURCE)) { this.map.removeSource(HIGHLIGHT_SOURCE); } }, resolveMarkerDetail(marker) { if (marker.detail) return marker.detail; if (marker.iso2 && this.countryDetails[marker.iso2]) { return this.countryDetails[marker.iso2]; } if (marker.name && this.countryDetails[marker.name]) { return this.countryDetails[marker.name]; } return null; }, ensureDetailModal() { if (this.detailModalBackdrop) return; const backdrop = document.createElement("div"); backdrop.className = "globe-detail-backdrop"; backdrop.hidden = true; const dialog = document.createElement("div"); dialog.className = "globe-detail-dialog"; dialog.setAttribute("role", "dialog"); dialog.setAttribute("aria-modal", "true"); const header = document.createElement("div"); header.className = "globe-detail-dialog-header"; const titleWrap = document.createElement("div"); titleWrap.className = "globe-detail-dialog-heading"; const titleEl = document.createElement("h2"); titleEl.className = "globe-detail-dialog-title"; const pathwayEl = document.createElement("p"); pathwayEl.className = "globe-detail-dialog-pathway"; titleWrap.appendChild(titleEl); titleWrap.appendChild(pathwayEl); const closeBtn = document.createElement("button"); closeBtn.type = "button"; closeBtn.className = "globe-detail-dialog-close"; closeBtn.setAttribute("aria-label", "Close country details"); closeBtn.textContent = "×"; closeBtn.addEventListener("click", () => this.closeCountryDetailModal()); header.appendChild(titleWrap); header.appendChild(closeBtn); const body = document.createElement("div"); body.className = "globe-detail-dialog-body markdown-body"; dialog.appendChild(header); dialog.appendChild(body); backdrop.appendChild(dialog); backdrop.addEventListener("click", (event) => { if (event.target === backdrop) { this.closeCountryDetailModal(); } }); this._detailModalKeyHandler = (event) => { if (event.key === "Escape" && !backdrop.hidden) { this.closeCountryDetailModal(); } }; document.addEventListener("keydown", this._detailModalKeyHandler); document.body.appendChild(backdrop); this.detailModalBackdrop = backdrop; this.detailModalTitle = titleEl; this.detailModalPathway = pathwayEl; this.detailModalBody = body; }, async openCountryDetailModal(detail) { if (!detail?.sections) return; this.ensureDetailModal(); this.detailModalTitle.textContent = detail.country || "Country details"; this.detailModalPathway.textContent = detail.pathway || ""; this.detailModalPathway.hidden = !detail.pathway; const parts = []; for (const [key, heading] of DETAIL_SECTIONS) { const content = detail.sections[key]; if (content) { parts.push(`### ${heading}\n\n${content}`); } } const markdown = parts.join("\n\n"); this.detailModalBody.innerHTML = await renderMarkdownHtml(markdown); for (const link of this.detailModalBody.querySelectorAll("a")) { link.target = "_blank"; link.rel = "noopener noreferrer"; } this.detailModalBackdrop.hidden = false; }, closeCountryDetailModal() { if (this.detailModalBackdrop) { this.detailModalBackdrop.hidden = true; } }, buildPopupElement(marker) { const detail = this.resolveMarkerDetail(marker); const status = detail?.status || "pending"; const container = document.createElement("div"); container.className = `globe-marker-popup globe-marker-popup--${status}`; const title = document.createElement("h4"); title.className = "globe-marker-popup-title"; title.textContent = detail?.country || marker.name || marker.iso2 || "Country"; container.appendChild(title); if (detail?.pathway) { const pathwayEl = document.createElement("p"); pathwayEl.className = "globe-marker-popup-pathway"; pathwayEl.textContent = detail.pathway; container.appendChild(pathwayEl); } if (status === "researching") { const statusBadge = document.createElement("p"); statusBadge.className = "globe-marker-popup-status"; statusBadge.textContent = "Research in progress"; container.appendChild(statusBadge); const activity = document.createElement("p"); activity.className = "globe-marker-popup-activity"; activity.textContent = `Researching: ${ detail?.current_activity || "Gathering official immigration sources…" }`; container.appendChild(activity); if (detail?.activities?.length > 1) { const list = document.createElement("ul"); list.className = "globe-marker-popup-activities"; for (const item of detail.activities.slice(-3)) { const listItem = document.createElement("li"); listItem.textContent = item; list.appendChild(listItem); } container.appendChild(list); } } else if (status === "complete") { if (detail?.preview) { const preview = document.createElement("p"); preview.className = "globe-marker-popup-preview"; preview.textContent = detail.preview; container.appendChild(preview); } if (detail?.sections && Object.keys(detail.sections).length) { const viewBtn = document.createElement("button"); viewBtn.type = "button"; viewBtn.className = "globe-marker-popup-view-details"; viewBtn.textContent = "View details"; viewBtn.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); void this.openCountryDetailModal(detail); }); container.appendChild(viewBtn); } } else if (status === "incomplete") { const incomplete = document.createElement("p"); incomplete.className = "globe-marker-popup-incomplete"; incomplete.textContent = detail?.preview || "Research did not produce verified findings for this country."; container.appendChild(incomplete); } else { const waiting = document.createElement("p"); waiting.className = "globe-marker-popup-waiting"; waiting.textContent = "Waiting for research to start…"; container.appendChild(waiting); } const hint = document.createElement("p"); hint.className = "globe-marker-popup-hint"; if (status === "researching") { hint.textContent = "Live tool activity from the country researcher."; } else if (status === "complete") { hint.textContent = "Verify key requirements on official immigration sites."; } else if (status === "incomplete") { hint.textContent = "Open the parallel research panel in chat for tool logs."; } else { hint.textContent = "Details appear as each country research completes."; } container.appendChild(hint); return container; }, createMarkerPopup(marker) { const popupNode = this.buildPopupElement(marker); const popup = new maplibregl.Popup({ offset: 24, closeButton: true, closeOnClick: true, maxWidth: "360px", className: "borderless-country-popup", }).setDOMContent(popupNode); return popup; }, addMarkers(markers) { if (!this.map || !markers?.length) return; markers.forEach((marker) => { const popup = this.createMarkerPopup(marker); const instance = new maplibregl.Marker({ color: "#f59e0b" }) .setLngLat([marker.lng, marker.lat]) .setPopup(popup) .addTo(this.map); instance.getElement()?.classList.add("globe-country-marker"); instance.getElement()?.setAttribute("role", "button"); instance.getElement()?.setAttribute( "aria-label", `View migration details for ${marker.name || marker.iso2}`, ); this.markers.push(instance); this.markerRecords.push({ marker, popup, instance }); }); }, updateMarkerPopups(countryDetails) { this.countryDetails = countryDetails || {}; for (const record of this.markerRecords) { const mergedMarker = { ...record.marker, detail: this.resolveMarkerDetail(record.marker), }; record.marker = mergedMarker; const popupNode = this.buildPopupElement(mergedMarker); record.popup.setDOMContent(popupNode); } }, addHighlights(highlights) { if (!this.map || !highlights?.length) return; const features = highlights.map((item) => ({ type: "Feature", geometry: { type: "Point", coordinates: [item.lng, item.lat], }, properties: { iso2: item.iso2, }, })); this.map.addSource(HIGHLIGHT_SOURCE, { type: "geojson", data: { type: "FeatureCollection", features, }, }); this.map.addLayer({ id: HIGHLIGHT_LAYER, type: "circle", source: HIGHLIGHT_SOURCE, paint: { "circle-radius": [ "interpolate", ["linear"], ["zoom"], 1, 18, 4, 28, 8, 42, ], "circle-color": "#f59e0b", "circle-opacity": 0.28, "circle-stroke-width": 2, "circle-stroke-color": "#fbbf24", "circle-stroke-opacity": 0.9, }, }); }, applyFlyTo(flyTo) { if (!this.map || !flyTo) return; if (flyTo.bounds) { this.map.fitBounds(flyTo.bounds, { padding: flyTo.padding ?? 80, duration: 1800, essential: true, }); return; } if (flyTo.center) { this.map.flyTo({ center: flyTo.center, zoom: flyTo.zoom ?? 4, duration: 1800, essential: true, }); } }, applyState(state) { if (!state) return; this.pendingState = state; if (!this.map) return; const versionChanged = state.version !== this.lastVersion; const detailsChanged = (state.details_version || 0) !== this.lastDetailsVersion; if (!versionChanged && !detailsChanged) return; this.countryDetails = state.country_details || {}; if (versionChanged) { this.lastVersion = state.version; this.clearMarkers(); this.clearHighlights(); this.addMarkers(state.markers || []); this.addHighlights(state.highlights || []); this.updateShellState(Boolean(state.markers?.length || state.highlights?.length)); this.applyFlyTo(state.fly_to); } else if (detailsChanged) { this.updateMarkerPopups(this.countryDetails); } this.lastDetailsVersion = state.details_version || 0; }, onMapReady() { if (this.pendingState) { this.applyState(this.pendingState); } }, }; function globeRootElement() { if (typeof element !== "undefined") { return element; } return document.querySelector(".globe-root"); } async function initGlobe(options = {}) { const root = globeRootElement(); if (!root) { return; } try { await ensureMapLibre(); } catch (err) { console.error("Globe failed to load MapLibre GL:", err); root.querySelector(".globe-shell")?.classList.remove("is-loading"); return; } const container = root.querySelector(".globe-map"); if (!container || container.dataset.mapInit) return; container.dataset.mapInit = "1"; const opts = mergeGlobeOptions(readOptionsFromElement(container), options); const { projection, attributionControl, scrollZoom, dragRotate, dragPan, keyboard, doubleClickZoom, touchZoomRotate, interactive, showGeolocateControl, useCurrentLocation, geolocateZoom, showUserLocation, ...mapOptions } = opts; const map = new maplibregl.Map({ container, attributionControl, scrollZoom, dragRotate, dragPan, keyboard, doubleClickZoom, touchZoomRotate, interactive, ...mapOptions, }); window.BorderlessGlobe.map = map; window.BorderlessGlobe.rootElement = root; let userMarker = null; if (showGeolocateControl) { map.addControl( new maplibregl.GeolocateControl({ positionOptions: { enableHighAccuracy: true }, trackUserLocation: false, showUserLocation, showAccuracyCircle: false, }), "top-right" ); } function flyToCurrentLocation() { if (!navigator.geolocation) return; navigator.geolocation.getCurrentPosition( (pos) => { const center = [pos.coords.longitude, pos.coords.latitude]; map.flyTo({ center, zoom: geolocateZoom, essential: true, }); if (showUserLocation) { if (!userMarker) { userMarker = new maplibregl.Marker({ color: "#3b82f6" }) .setLngLat(center) .addTo(map); } else { userMarker.setLngLat(center); } } }, (err) => console.warn("Geolocation failed:", err.message), { enableHighAccuracy: true } ); } map.on("style.load", () => { map.setProjection({ type: projection }); map.resize(); }); map.on("load", () => { root.querySelector(".globe-shell")?.classList.remove("is-loading"); root.querySelector(".globe-shell")?.classList.add("is-ready"); map.resize(); if (useCurrentLocation && !window.BorderlessGlobe.pendingState?.fly_to) { flyToCurrentLocation(); } window.BorderlessGlobe.onMapReady(); }); window.addEventListener("resize", () => map.resize()); return map; } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => initGlobe()); } else { initGlobe(); }