/* =================================================== JAESWIFT.XYZ — 3D Globe (Globe.gl) eDEX-UI inspired rotating globe with server location =================================================== */ (function () { 'use strict'; const container = document.getElementById('globeViz'); if (!container || typeof Globe === 'undefined') return; // Manchester coordinates const SERVER_LAT = 53.48; const SERVER_LNG = -2.24; // Wait for container to be visible and sized function initGlobe() { const rect = container.parentElement.getBoundingClientRect(); const size = Math.min(rect.width, 340); if (size < 50) { setTimeout(initGlobe, 200); return; } const globe = Globe() .globeImageUrl('') .backgroundColor('rgba(0,0,0,0)') .width(size) .height(size) .showAtmosphere(true) .atmosphereColor('#00ff44') .atmosphereAltitude(0.2) // Hex polygons for land masses .hexPolygonsData([]) // Points - server location .pointsData([{ lat: SERVER_LAT, lng: SERVER_LNG, size: 0.6, color: '#00cc33', label: 'MANCHESTER' }]) .pointAltitude('size') .pointColor('color') .pointRadius(0.4) .pointsMerge(false) // Rings - pulse effect .ringsData([{ lat: SERVER_LAT, lng: SERVER_LNG, maxR: 3, propagationSpeed: 1.5, repeatPeriod: 1200 }]) .ringColor(function () { return '#00cc3380'; }) .ringMaxRadius('maxR') .ringPropagationSpeed('propagationSpeed') .ringRepeatPeriod('repeatPeriod') // Labels .labelsData([{ lat: SERVER_LAT, lng: SERVER_LNG, text: 'MCR // 53.48°N', color: '#00cc33', size: 0.7 }]) .labelColor('color') .labelSize('size') .labelDotRadius(0.3) .labelAltitude(0.01) .labelText('text') .labelResolution(2) // Custom globe material .onGlobeReady(function () { // Style the globe mesh var globeMesh = globe.scene().children.find(function (c) { return c.type === 'Mesh' && c.geometry && c.geometry.type === 'SphereGeometry'; }); if (globeMesh && globeMesh.material) { globeMesh.material.color.setHex(0x0a1a0a); globeMesh.material.emissive.setHex(0x008800); globeMesh.material.emissiveIntensity = 0.8; } }) (container); // Auto rotate globe.controls().autoRotate = true; globe.controls().autoRotateSpeed = 0.4; globe.controls().enableZoom = false; globe.controls().enablePan = false; globe.controls().minPolarAngle = Math.PI * 0.35; globe.controls().maxPolarAngle = Math.PI * 0.65; // Point camera at Manchester globe.pointOfView({ lat: SERVER_LAT, lng: SERVER_LNG, altitude: 2.2 }, 1500); // Fetch country polygons for land outlines fetch('https://unpkg.com/world-atlas@2/countries-110m.json') .then(function (r) { return r.json(); }) .then(function (worldData) { var countries = topojsonFeature(worldData, worldData.objects.countries).features; globe .hexPolygonsData(countries) .hexPolygonResolution(3) .hexPolygonMargin(0.4) .hexPolygonColor(function () { return 'rgba(0, 220, 50, 0.55)'; }) .hexPolygonSideColor(function () { return 'rgba(0, 255, 60, 0.6)'; }) .hexPolygonAltitude(0.005); }) .catch(function () { // Fallback: no country data, globe still works }); // ─── LIVE TRAFFIC ARCS — fetched from /api/visitor/recent-arcs ─── var currentArcs = []; function parseNginxTs(ts) { // '20/Apr/2026:03:35:38 +0200' → Date if (!ts) return null; try { var parts = ts.match(/(\d+)\/(\w+)\/(\d+):(\d+):(\d+):(\d+)\s*([+-]\d{4})?/); if (!parts) return new Date(ts); var months = { Jan:0,Feb:1,Mar:2,Apr:3,May:4,Jun:5,Jul:6,Aug:7,Sep:8,Oct:9,Nov:10,Dec:11 }; return new Date(Date.UTC( parseInt(parts[3]), months[parts[2]] || 0, parseInt(parts[1]), parseInt(parts[4]), parseInt(parts[5]), parseInt(parts[6]) )); } catch (e) { return null; } } function timeAgo(ts) { var d = parseNginxTs(ts); if (!d) return '—'; var s = Math.floor((Date.now() - d.getTime()) / 1000); if (s < 60) return s + 's ago'; if (s < 3600) return Math.floor(s / 60) + 'm ago'; if (s < 86400) return Math.floor(s / 3600) + 'h ago'; return Math.floor(s / 86400) + 'd ago'; } function buildArcsFromVisitors(visitors) { return (visitors || []).map(function (v) { // jitter lat/lon slightly so duplicates from same country don't overlap perfectly var jlat = (Math.random() - 0.5) * 1.2; var jlon = (Math.random() - 0.5) * 1.2; return { startLat: v.lat + jlat, startLng: v.lon + jlon, endLat: SERVER_LAT, endLng: SERVER_LNG, color: ['rgba(0, 255, 60, 0.85)', 'rgba(20, 241, 149, 0.15)'], country_code: v.country_code, country_name: v.country_name, page_viewed: v.page_viewed, timestamp: v.timestamp, label: (v.country_name || v.country_code || 'Unknown') + ' → MCR · ' + timeAgo(v.timestamp) + ' · ' + (v.page_viewed || '/') }; }); } function fallbackArcs() { var cities = [ { lat: 40.71, lng: -74.01, name: 'New York' }, { lat: 35.68, lng: 139.69, name: 'Tokyo' }, { lat: 48.86, lng: 2.35, name: 'Paris' }, { lat: -33.87, lng: 151.21, name: 'Sydney' }, { lat: 55.76, lng: 37.62, name: 'Moscow' }, { lat: 1.35, lng: 103.82, name: 'Singapore' }, { lat: 52.52, lng: 13.41, name: 'Berlin' }, { lat: 34.05, lng: -118.24, name: 'LA' } ]; return cities.map(function (c) { return { startLat: c.lat, startLng: c.lng, endLat: SERVER_LAT, endLng: SERVER_LNG, color: ['rgba(0, 255, 60, 0.55)', 'rgba(0, 255, 60, 0.05)'], label: c.name + ' → MCR (demo)' }; }); } function refreshArcs() { fetch('/api/visitor/recent-arcs', { cache: 'no-store' }) .then(function (r) { return r.ok ? r.json() : []; }) .then(function (visitors) { if (!Array.isArray(visitors) || !visitors.length) { currentArcs = fallbackArcs(); } else { currentArcs = buildArcsFromVisitors(visitors.slice(0, 50)); } globe.arcsData(currentArcs); var countEl = document.getElementById('globeArcCount'); if (countEl) countEl.textContent = String(currentArcs.length); }) .catch(function () { if (!currentArcs.length) { currentArcs = fallbackArcs(); globe.arcsData(currentArcs); } }); } globe .arcsData([]) .arcColor('color') .arcDashLength(0.5) .arcDashGap(0.2) .arcDashInitialGap(function () { return Math.random(); }) .arcDashAnimateTime(2200) .arcStroke(0.45) .arcAltitudeAutoScale(0.38) .arcLabel('label'); refreshArcs(); setInterval(refreshArcs, 30000); // Handle resize var resizeTimer; window.addEventListener('resize', function () { clearTimeout(resizeTimer); resizeTimer = setTimeout(function () { var newRect = container.parentElement.getBoundingClientRect(); var newSize = Math.min(newRect.width, 340); globe.width(newSize).height(newSize); }, 250); }); } // Topojson helper function topojsonFeature(topology, obj) { if (!obj || !obj.geometries) return { type: 'FeatureCollection', features: [] }; return { type: 'FeatureCollection', features: obj.geometries.map(function (geom) { return { type: 'Feature', geometry: topojsonGeometry(topology, geom), properties: geom.properties || {}, id: geom.id }; }) }; } function topojsonGeometry(topology, obj) { var arcs = topology.arcs; var transform = topology.transform; function decodeArc(arcIdx) { var arc = arcs[arcIdx < 0 ? ~arcIdx : arcIdx]; var coords = []; var x = 0, y = 0; for (var i = 0; i < arc.length; i++) { x += arc[i][0]; y += arc[i][1]; var lon = x, lat = y; if (transform) { lon = lon * transform.scale[0] + transform.translate[0]; lat = lat * transform.scale[1] + transform.translate[1]; } coords.push([lon, lat]); } if (arcIdx < 0) coords.reverse(); return coords; } function decodeRing(arcIndices) { var coords = []; for (var i = 0; i < arcIndices.length; i++) { var arcCoords = decodeArc(arcIndices[i]); if (i > 0) arcCoords.shift(); coords = coords.concat(arcCoords); } return coords; } var type = obj.type; if (type === 'Polygon') { return { type: 'Polygon', coordinates: obj.arcs.map(decodeRing) }; } else if (type === 'MultiPolygon') { return { type: 'MultiPolygon', coordinates: obj.arcs.map(function (polygon) { return polygon.map(decodeRing); }) }; } else if (type === 'GeometryCollection') { return { type: 'GeometryCollection', geometries: (obj.geometries || []).map(function (g) { return topojsonGeometry(topology, g); }) }; } return { type: type, coordinates: [] }; } // Init when DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function () { setTimeout(initGlobe, 300); }); } else { setTimeout(initGlobe, 300); } })();